Bypass Encapsulation
The overview
The Whitebox class provides a set of methods that can help you bypass encapsulation if needed. In general, it’s not a good idea to get/modify non-public fields, but sometimes it’s the only way to test code to cover it for future refactoring.
- use
Whitebox.setInternalState(..)
To set a non-public member of an instance or class. - use
Whitebox.getInternalState(..)
To get a non-public member of an instance or class. - use
Whitebox.invokeMethod(..)
To call a non-public method of an instance or class. - use
Whitebox.invokeConstructor(..)
Creates an instance of a class from a private constructor.
The sample
Accessing internal state
For mutable objects, the internal state may change after a method is called. When unit testing such objects, it would be nice to have a simple way to keep this state and see if it has been updated accordingly. PowerMock provides several useful reflection tools designed specifically for unit testing. All these reflect tool is located in the org. Powermock. Reflect the Whitebox class.
For demonstration purposes, we assume a class that looks like this:
public class ServiceHolder {
private final Set<Object> services = new HashSet<Object>();
public void addService(Object service) {
services.add(service);
}
public void removeService(Object service) { services.remove(service); }}Copy the code
Suppose we want to test the addService method (sure, it’s “too easy,” but it’s enough to test it for demonstration purposes). Here, we want to make sure that the status of ServiceHolder is updated correctly after calling the addService method. That is, the new object has been added to the Services collection. One way is to add a package-private or protected method called getServices() that returns the collection of services. But by doing so, we add a method to the ServiceHolder class that serves no other purpose than to make the class testable. This method can also be misused elsewhere in the code. Another option is to use the Whitebox PowerMock. GetInternalState (..) Method to do the same thing without changing production code. In this simple case, the test for the entire addService method looks like this:
@Test
public void testAddService(a) throws Exception {
ServiceHolder tested = new ServiceHolder();
final Object service = new Object();
tested.addService(service);
// This is how you get the private services set using PowerMock
Set<String> services = Whitebox.getInternalState(tested,
"services");
assertEquals("Size of the \"services\" Set should be 1".1, services
.size());
assertSame("The services Set should didn't contain the expect service",
service, services.iterator().next());
}
Copy the code
With PowerMock 1.0 or later, you can also get internal state by specifying the type of field to get. In this case, we could write it like this
Set<String> services = Whitebox.getInternalState(tested, Set.class);
Copy the code
To get the services field. This is a more type-safe way to get internal state and is the preferred method. In cases where there are multiple fields of type Set in a class, you still have to revert to using field name methods.
Setting the internal state of an object is equally easy. For example, we have the following classes:
public class ReportGenerator {
@Injectable
private ReportTemplateService reportTemplateService;
public Report generateReport(String reportId) {
String templateId = reportTemplateService.getTemplateId(reportId);
/* * Imagine some other code here that generates the report based on the * template id. */
return new Report("name"); }}Copy the code
Here, we imagine that we are using a dependency injection framework that automatically provides us with ReportTemplateService instances at run time. We have a number of options for testing this. For example, we can refactoring the class to use constructor or setter injection, using Java reflection, or we can simply create a setter for reportTemplateService that is only used for testing purposes (that is, setting up mock mock instances). Another way is to let PowerMock use Whitebox. SetInternalState (..) Method performs reflection for you. In this case, it’s really easy:
Whitebox.setInternalState(tested, "reportTemplateService", reportTemplateServiceMock);
Copy the code
Using PowerMock 1.0 or later makes it easier because you can even ignore field names:
Whitebox.setInternalState(tested, reportTemplateServiceMock);
Copy the code
This is the preferred method because it is more refactoring friendly. If you have multiple ReportTemplateService type fields in your class, you may still have to revert to the field name method.
The set and GET internal state methods we just saw also have some more advanced use cases. Many times, it is useful to modify or read the internal state in an object’s class hierarchy. Typically, setInternalState and getInternalState traverse the superclass hierarchy to look for fields in subclasses, and then return or set the first field that matches the field name provided. But in some cases, you do want to get or set a specific field at a place in the hierarchy. For example, suppose we have an instance of class A that extends class B, and both A and B have A java.lang.String field named myPrivateString, but in this case we want to read the contents of B’s field. By using the Whitebox. GetInternalState (..) We can easily do this by specifying where fields should be read from the class hierarchy. In this case, we would write the following:
String myPrivateString = Whitebox.<String> getInternalState(instanceOfA, "myPrivateString", B.class);
Copy the code
Or, if you are using PowerMock 1.0 or later:
String myPrivateString = Whitebox.getInternalState(instanceOfA, String.class, B.class);
Copy the code
In most cases, this also eliminates casts.
Using the first method, the conversion can be avoided by specifying the return type:
String myPrivateString = Whitebox.getInternalState(instanceOfA, "myPrivateString", B.class, String.class);
Copy the code
If we changed to setting the state of myPrivateString, we would do this:
Whitebox.setInternalState(instanceOfA, "myPrivateString"."this is my private string", B.class);
Copy the code
Or, if you are using PowerMock 1.0 or later:
Whitebox.setInternalState(instanceOfA, String.class, "this is my private string", B.class);
Copy the code
Calling private methods
To call a private method, use whitebox.invokemethod (..) in PowerMock. Methods. For example, suppose you have a private method called sum in instance myInstance and want to test it separately:
private int sum(int a, int b) { returna+b; }Copy the code
To do this, you simply do the following:
int sum = Whitebox.<Integer> invokeMethod(myInstance, "sum".1.2);
Copy the code
Then print the sum and it will show 3.
This works fine in most cases. However, in some cases PowerMock cannot use this simple syntax to figure out which method to call. For example, if an overloaded method uses a primitive type and another method uses a wrapper type, for example:
.private int myMethod(int id) { return 2*id; }private int myMethod(Integer id) { return 3*id; }...Copy the code
This is certainly a naive example, but you can still use whitebox.invokemethod (..) Call both methods. In the case of myMethod(int ID), you would do something like this:
int result = Whitebox.<Integer> invokeMethod(myInstance, newClass<? > [] {int.class}, "myMethod".1);
Copy the code
Here we explicitly tell PowerMock to expect a call to myMethod with an int as an argument. But in most cases, you don’t need to specify parameter types because PowerMock will automatically find the correct method.
You can also invoke class-level (static) methods. Assuming that the sum method is also static, in which case we can call it as follows:
int sum = Whitebox.<Integer> invokeMethod(MyClass.class, "sum".1.2);
Copy the code
If the method is defined in a class named MyClass.
Starting with PowerMock 1.2.5, you can also call a method without specifying its name, which is useful for refactoring purposes. For example,
Whitebox.invokeMethod(myInstance, param1, param2);
Copy the code
Note that this only works if PowerMock can locate unique methods based on parameter types.
Instantiate a class with a private constructor
Instantiate a class with a private constructor, for example:
public class PrivateConstructorInstantiationDemo { private final int state; private PrivateConstructorInstantiationDemo(int state) { this.state = state; } public int getState(a) { return state; }}
Copy the code
You only need to:
PrivateConstructorInstantiationDemo instance = WhiteBox.invokeConstructor( PrivateConstructorInstantiationDemo.class, 43);
Copy the code
43 is the parameter of state.
This works fine in most cases. However, in some cases PowerMock cannot use this simple syntax to figure out which constructor to call. For example, if an overloaded constructor uses a primitive type and another constructor uses a wrapper type, for example:
public class PrivateConstructorInstantiationDemo { private final int state; private PrivateConstructorInstantiationDemo(int state) { this.state = state; } private PrivateConstructorInstantiationDemo(Integer state) { this.state = state; // do something else } public int getState() { return state; }}
Copy the code
This, of course, is a naive sample, but you can still use the Whitebox. InvokeConstructor (..) To call the two constructors. For the Integer parameter constructor, you can do this:
PrivateConstructorInstantiationDemo instance = Whitebox.invokeConstructor(PrivateConstructorInstantiationDemo.class, newClass<? >[]{Integer.class},43);
Copy the code
Here, we explicitly tell PowerMock to expect a call to the constructor that takes Integer as an argument. But in most cases, you don’t need to specify parameter types because PowerMock will automatically find the correct method.
Pay attention to
All of these things can be done without using PowerMock, which is just normal Java reflection. Reflection, however, requires a lot of boilerplate code and is prone to error, so PowerMock can provide you with these utility methods. PowerMock lets you choose whether to refactor your code and add getter/setter methods to check/change internal state, or whether to use its utility methods to do the same thing without changing the production code. It’s up to you!
reference
- ServiceHolderTest
- ServiceHolder
- ReportGeneratorTest
- ReportGenerator
- ReportDaoTest
- ReportDao