Design for Testability
Jakob Jenkov |
Designing for testability means designing your code so that it is easier to test. To do so, you may have to break with some of the principles we learned in university, like encapsulation.
Why Encapsulation Makes Testing Harder
Encapsulation is one of those principles that look great on paper, but in reality turn out not always to be without problems. I have alsow commented on this in the text Optional Abstractions in my API Design trail.
Below is a diagram illustrating a unit test, a unit (which is being tested), and a dependency which is some class used by the unit. If the unit was a piece of business logic, the dependency could be a DAO, or some other component needed to carry out the business logic.
A Unit test, unit and dependency. |
To test the unit you usually call its methods and make assertions on the result returned. However, if a method called does not return a result, but rather causes some side effect in another component or database, it is harder to test if the unit works correctly.
Replaceable Dependencies
The first thing you can do to make a unit easier to test, is to allow its dependencies to be set from the outside. Thus, you can inject mock dependencies instead of the real dependencies. Using these mock objects you can record how the unit uses the dependency, and then make assertions on that, to make sure the unit works correctly. I have written about this in more detail in the text "Stub, Mock and Proxy Testing.
Turning Private into Protected
If a method or field is private, only instances of that class can access that method or field. A protected method or field, on the other hand, is also accessible to other classes in the same package, and for subclasses of the unit too.
If the unit test is located in the same package as the unit to test (though still under a different root directory), the unit test can actually access protected methods and fields of the unit. That makes it easier to make assertions about the results of internal methods, and the internal state of the unit.
That a method is protected rather than private still signals to the user, that this is probably not a method that should be used directly by clients of the unit, even if you do so from your unit tests.
Encapsulating Calls to Dependencies in Protected Methods
By encapsulating calls to external dependencies (components) in protected methods, you can create a subclass mock of the unit to test, and override these protected methods, to make them record information about whether they were called or not.
I have addressed this technique in the text Subclass Mocks.
Move Code out of Boundary Classes
Boundary classes may some times be hard to test. By "boundary class" I mean classes that plug into some framework. You may need the whole framework running in order to test your boundary class.
Examples of boundary classes are Servlets, Struts actions, custom Swing components, EJB's etc. Each of these boundary classes need a larger framework or server running in order to test them.
A way to get around that is to move the business logic out of the boundary class, and into it's own component. The boundary class will then call this component. The thread of execution will then look something like this:
boundaryClass --> business logic component --> ...
Make sure that the business logic component does not know anything about the boundary classes.
For instance, if your boundary class is a servlet, your business logic class should not know anything about the
HttpRequest
or HttpResponse
interfaces.
Preferably, the business logic class only takes the parameters it needs, as the types it needs. For instance, if it needs an ID sent in the request, the servlet will take the ID out of the request, convert it to a long, and pass it to the business logic component.
When your business logic class is separated from the boundary class, you can test the business logic class separately. This is typically easier, than trying to unit test e.g. a servlet or Struts action. When the boundary classes are minimized to dispatch logic, the risk of errors in them are a lot smaller, in case you choose not to unit test them.
For an example of this, see the Servlet Unit Testing text, in which I show how to unit test the business logic of a servlet, by moving the business logic to a separate class.
Tweet | |
Jakob Jenkov |