More often than not, enterprise code is badly unit tested or worse, not unit tested. While the reasons for this could vary from time constraints, budget constraints or lack of developer knowledge, surprisingly many times it is because the design of the application was rigid or poor not allowing writing unit tests. Test driven development (TDD) proliferated by agile methodology has not yet been completely embraced by all enterprises. As long as unit tests are written after the code has been developed (and thus after the design), developers run into the problem of unit testing with ease.
Unit tested code reduces the defect density in an application. The motive of this article is not to elucidate the benefits of unit testing (see references for that), but to provide a collection of good practices to design an application keeping in mind the ability to unit test. Many teams spend considerable amount of time "designing" considering the aspects of layering, complexities, patterns and loose coupling. Loose coupling definitely aids in writing unit tests with ease. This article gathers the ideas of loose coupling that lead to a good design which in turn leads to writing good unit tests. It also captures the process which allows developers to write unit tests. This is useful for those developers who design first and develop later.
Designing for unit testing
The key to writing good unit tests starts with a good design. Design should facilitate unit testing. A design thought out on solid design principles like creating clean interfaces, composing objects correctly, using dependencies properly eventually help in writing and mocking tests.
Unit tests are pieces of code that try to verify if the real/actual code is written correctly as per the expected documentation. It also has to check whether the unit runs into unexpected problems. In order for them to do so, they should be as less obtrusive as possible. The developer should not keep changing the real code to run the unit test. The idea of unit test is that it should be executed on a standalone JVM without the availability of a server.
To explain better, let us say there is a method M() in a class A. M() depends on another method N() from another class B. To unit test M(), it is necessary that method N() is also executed and runs as a standalone java class. Running N() could have further complications if it depends on other methods from some other classes. So there are two options to unit test M():
- Option 1 - Change the code in method N() to return results as per method M() expects. This would mean actually commenting out the ‘real’ code and writing code to return expected values. This is a very intrusive way of programming as it is modifying a real code. I have seen people do this when they do not write unit tests but want to test their code.
- Option 2 - If this scenario is designed correctly, meaning that class B was designed as an interface and there was an implementation class BImpl with the implementation of method N(), it would be much easier to create another class say UnitTestImplm having a dummy implementation of method N(). Now there is a chance of method M()using the UnitTestImpl’s method N() instead of BImpl’s (with some setup still). This way the real code N() from class BImpl will never need to be changed. The is less intrusive as the real code of N() in BImpl is never changed, just a new implementation of N() in UnitTestImpl is added.
Of the two options mentioned, if the system was designed using option 2, then it gives the developer the provision to write and execute unit tests without actually commenting or deleting the real code. This is a very important requirement for unit testing.Program to an interface not an implementation
This is an old adage (from GoF) but missing in enterprise code. The dependencies between classes should be considered via an interface. Interfaces decouple an implementation class from its dependency. The idea of this is that a concrete implementation class can be swapped with another implementation, maybe for unit testing and still the dependant class is unaware of the exact implementation class. Programming to an interface allows multiple implementations of the same methods, which aids in developing mock classes that are helpful for unit testing.
During the high-level design phase identify classes (entities) and interfaces. Interfaces have to be thought out in the following scenarios:
- If there are classes participating as dependencies in other classes
- If there are multiple inheritances
- If there is a possibility of having multiple implementation (for example multiple implementation of Database calls using different frameworks)
- Classes that interact between different layers/tiers (for example service classes from service tier and action classes from web tier)
Having identified interfaces, use them in the class diagrams instead of the concrete implementation. The interfaces show the vocabulary of the system. A unit test developer should read the documentation and contract of the interface to understand the behavior of the system. The concrete class is just an instance of the contract expressed.Favor object composition over inheritance
Consider designing a system via composition over only just inheritances. Generalization should not be used unless a class is really a sub type of another class. If more than one class share the same behavior they need not be abstracted unless they have some common ‘type’ or behavior between them. An alternate approach to this would be to move the common behavior into another meaningful class and compose it as an object in the two classes.
Composition reduces coupling by allowing us to plug in a small thing into another can call back the smaller thing to get the work done. It is important to note that composition still favors inheritance. It’s just that there is a smaller interface to implement and not a big class to inherit from.
From a unit testing perspective it is easier to unit test classes that have composition rather than concrete implementations. For example if a Class A is composed of Class B. The unit testing of A is easy if:
- B has been designed with an interface
- A mock class exists for class B for unit testing A.
Unit testing in the presence of singletons can be difficult because of the state that must be managed between tests. Furthermore, the singleton objects might not have methods to allow a unit test to set up the state it needs or query the results afterwards. Developing with Mock Objects encourages a coding style where objects are passed into the code that needs them. This makes substitution possible and reduces the risk of unexpected side-effects.
Presence of unwanted singletons hampers the unit testing. For example consider a method M() from class A that depends on a method N() from a singleton class B. So the code inside method M() would be:
It would almost be impossible to unit test if method N() would not work as a standalone java class. This problem can be solved if B was not a singleton but instead an interface with multiple implementation. Method M() would get the exact implementation of B using either a factory or a dependency injection framework like Spring. This allows us to setup a unit test class.
Avoiding Singletons increases the flexibility of the application and makes unit testing simpler, faster, and generally more effective.Leverage the use of dependency frameworks
If the architecture permits the use of frameworks, consider using a dependency injection framework like Spring. Spring framework allows externalizing the dependencies by moving them to a configuration file (xml file). The advantages are:
- A nice clean way of managing dependencies
- Sparse use of singletons
- Easy to plug in and out different implementations of the dependencies
- In built support for unit testing
The method documentation should clearly state what the method is expected to do. If there are branches within the method, the documentation should explain the conditional logic. Consider giving as much documentation as possible to help the unit test developer to understand the method’s purpose.Define and document the method contract
Design by contract (DBC) is a way of writing the method documentation so that it clearly defines what the code will do. It should clearly capture the preconditions, post conditions and any invariants should be clearly documented.
The following tags can be used for method documents:
@pre – Conditions that have to be true before method can be called
@post – Conditions that should be true after the method gets executed
@invariant – This is a constraint on the object state that is guaranteed to be true at all times, such as Collection.size() == Collection.toArray().length().
In the detailed design phase when identifying methods and behavior of classes, also identify the contract of the method. This contract specifies terms under which the code will be executed. The contract defines the following and this has to be documented in the method specification.
- Input – Range of expected values for the input(s) of the method
- Output - Range of expected values for output(s) of the method
- Boundary - Clearly state the boundary conditions for the method. The conditions in which the method would fail (negative flows)
- Exception – The cases when the method would fail by an exception. Document the input values that could cause this. Document the expected behavior of the system when the method fails through an exception.
- Assumptions – Document the assumptions (if any) on how the method will behave. For example methods that deal with data in some formats should document the format. The assumption is that the data format would not change.
Before writing the unit tests
Before getting started on writing the unit tests, it is very important to read the method documentation and understand the purpose of the method. This would give a complete idea about what is to be tested. It is also necessary to formulate the various conditions that are bad or invalid and the different scenarios in which the method would run into errors. Sometimes when the method document is not sufficient it would be very useful to update the method document with all the details that are needed to unit test. Some pointers to keep in mind before starting the unit test are:
- Write the unit tests based on the documentation – It is important that the method documentation details the behavior of the method. If a public method does not give out all the necessary details, it might be a good idea to add that documentation as the developer. The idea is to adhere to the verbal contract of the method.
- Write the unit tests based on the code – Revisit the code to be unit tested. Understand if it is working as the documentation suggests. If there is a difference between the contract and the code, then one of them must be fixed. Once the code looks fine there would be a clear idea of how to unit test. The places where assertions and failures have to be tested could be understood.
- Write the unit tests based on the expected results – Test for all the scenarios – success, failure, boundary conditions and exceptions.
- Unit Test the assumptions – there would be assumptions in the code when there are dependencies. For example an API call could be assumed to work always. It is good to unit test the piece if the API did not function as defined. By this we can see if the code is able to recover from an error and not crash.
Advantages of thinking about unit testing upfront
Thinking about unit testing during design, leads to a good design. Unit tests are not just pieces to catch ‘bugs’, they also drive the design. Unit tests enforce the contract of the classes and methods and thus making sure the design adheres to the contract of the system. Some of the advantages of thinking of unit testing during design are following:
- Unit tests drive the developers to think about interfaces. This practice leads to writing loosely coupled code. It also benefits in writing good apis.
- Unit tests lead to better design by allowing a right mix of composition and interfaces
- Unit tests drive the need for good method documentation. Remember the standard: If your program isn’t worth documenting, it probably isn’t worth running (Nagler, 1995)
- Importance of unit testing - http://www.oreillynet.com/pub/a/oreilly/oracle/utplsql/news/fulldoc.html
- Design principles http://www.artima.com/lejava/articles/designprinciples.html
- Avoid Singletons – http://www.ibm.com/developerworks/webservices/library/co-single.html
- Unit testing with mocks - http://www.mockobjects.com/files/endotesting.pdf also http://www.ibm.com/developerworks/library/j-mocktest.html
- Testing the Data layer - https://www.theserverside.com/news/1365197/Testing-a-persistent-domain-model
- Coding standards http://www.ambysoft.com/essays/javaCodingStandards.html
- Importance of good documentation http://www.ibm.com/developerworks/library/j-jtp0821.html
- Introduction to design by contract http://archive.eiffel.com/doc/manuals/technology/contract/page.html