Saturday, January 2, 2016

Testing Private Methods in Unit Tests

Covering private code in unit tests can be problematic as they aren't directly executable from unit tests.  I've seen developers take one of two approaches.  One approach is to embed tests for private code in unit tests for protected or public methods.  Another is to escalate the declaration in the code being tested from private to protected, so that they can be more easily handled in unit tests.  Both approaches are problematic.

Testing private methods indirectly through protected or public methods is impractical.  If the private method has conditional logic or manipulates inaccessible fields, it can be hard to test each condition as it requires manipulating inputs to a protected or public method to do so.  In other words, testing of private methods becomes a "bank shot" more or less.  This takes additional time that developers don't always have.  It also makes test code more complex and more difficult to maintain.

Escalating methods and fields to protected status for testing creates a leakage of concerns issue.  Objects work like spies - on a "need to know basis".  If a method is going to be something other than private, then that escalated status should be needed in production code somehow.  If not, then the function of that private method wasn't really intended to be exposed.  The class might not be designed for that method to be overridden in an extension or called from outside the class in some other context.  Escalating that private method to protected in order to test it looses the documentation for other developers that it's dedicated to a specific purpose and really shouldn't be called from outside the class that defines it.

Apache Commons Lang and Reflection to the rescue.  Through reflection, access to public and private methods is more than possible.  Commons Lang makes that job easier.  In other words, you can have unit tests for private methods and check the value of private fields without escalating those items to protected status or attempting to test them indirectly.

Reading and Writing Private Fields

Interrogating and manipulating the values of private fields (without get and set methods) is fairly easy through the Commons Lang FieldUtils class.  Examples of reading and writing the values of private methods are provided in examples 1 and 2 respectively.  As you can see, FieldUtils gets this down to a one-liner.

Example 1:  Reading the value of a private field

Integer privateInt = (Integer) FieldUtils.readField(myClassInstance, "privateInt", true);
assertEquals(TEST_VALUE, privateInt);

Example 2:  Changing the value of a private field

FieldUtils.writeField(myClassInstance, "privateInt", Integer.valueOf(5), true);
// Run your tests and check for results


Executing Private Methods

Executing private methods *should* be just as easy from Commons Lang from the MethodUtils class.  In fact, I've proposed a minor enhancement to the Commons team to do just that. Until such a time, private methods can still be executed/tested directly. However, it's three lines of code and not just one.  I've got two illustrations of invoking private methods.  The first, example 3a, show executing a private method with no arguments.  The second, example 3b, is slightly more complex and assumes two arguments: a primitive int and a string.  Note that you do need to change the accessibility of the method so that it can be executed before you invoke it.

As I said, I hope that Commons Lang provides a one-line alternative for this at some point in the future.

Example 3a: Executing a private method (no arguments).

Method myPrivateMethod = MyClass.class.getDeclaredMethod("myPrivateMethod"); 
myPrivateMethod.setAccessible(true);
Object myResult = myPrivateMethod.invoke(myClassInstance);

Example 3b: Executing a private method (two arguments).

Method myPrivateMethod = MyClass.class.getDeclaredMethod("myPrivateMethod", Integer.TYPE, String.class); 
myPrivateMethod.setAccessible(true);
Object myResult = myPrivateMethod.invoke(myClassInstance, 5, "testValue");




No comments:

Post a Comment