To achieve an impeccable system design, write a failing test first, a simple implementation to pass the test, refactor and repeat. Sounds easy, right? Then why is it not so easy to get developers to adopt this technique on a regular, or better yet, permanent basis? One possible reason is because writing unit tests often becomes a burdensome task. Developers feel they have a better shot at meeting the always tight deadlines if they forego this mundane task. Even worse, the tests that do get written only seem to cause a nuisance, for when they break, it takes a considerable effort to decipher these unit pests
and often chunks of code become commented out. This is because the act of TDD takes more than simply following those three steps. It takes a deeper understanding of why TDD improves the design of software. Specifically, how a developer approaches unit testing determines if TDD will work for them and their team.
A key characteristic of TDD that works is test readability. By focusing on test readability, a developer is forced to think about the object under test in a way that will promote good design and provide valuable documentation for the components of the system. This article provides a few quick tips to ensuring that a unit test clearly expresses an object’s intent that I initially realized after reading “Growing Object Oriented Software, Guided by Test” by Steve Freeman and Nat Pryce.
First, the name of a unit test name goes a long way to identifying the object’s purpose. Often times, a unit test is named by preceding an object’s method with “test.” This is troublesome for multiple reasons. First, the method should not exist yet if one is following the very important step of writing a failing test before writing any other line of code for that object. If the developer is predicting what that method name will be, they are also approaching the unit test in the wrong frame of mind. The developer should be thinking about the behavior of the object, the boundaries of that object, and how the object will be used. Thinking about a method name is thinking about how one will implement the object. This is a very important distinction when trying to benefit from TDD. Thinking about an object’s implementation when writing a test first is no different than actually writing the implementation first. Cohesion and readability are forced on the objects implementation when the developer is thinking about an object behavior first. Therefore, tests should be named precisely after the behavior they are testing. One technique created by Chris Stevenson, the author of a documentation generator for Java called TestDox, is to name the test methods as a sentence describing the objects behavior where the subject of the sentence is implicitly the object under test. For example, given a class that handles updating a documents workflow related properties when the document queue is changed (ProcessingInfoQueueNameChangeHandler)
It should be clear what behavior each of these methods is testing. More importantly, it should be clear as to what the object under tests does. It is important to note that it’s ok that these method names are quite long and you would never want to use such names in your production code. This is because these methods will never be explicitly called as Junit will use reflection to find and execute these methods.
Another tip to improving the readability of your test code is to use Hamcrest matchers for your assertions. Hamcrest is a library of matcher objects used in various testing frameworks such as JUnit, TestNG, Mockito, and JMock. Test readability is improved with use of Hamcrest as it provides a more natural reading of the test assertions. In a nutshell, to use Hamcrest you use the following assertions:
assertThat([value], [matcher statement]);
It is also important to note that it is easy to create your own custom matchers as well and this is recommended when you find multiple instances of the same conditional code in your assertions.
Lastly, it should be clear what the literal values in your test code mean. It is difficult for someone to determine if the literal values you happen to use in your test are random or are important to the context of the test and its results. Therefore, it’s better to define literal values as constant variable where the variable names define precisely what is meant by the value. For example:
This is much more clear than if ANY_QUEUE_NAME was replaced by something like “Initial Review.”
Hopefully, these 3 tips will get you on the path to creating unit test that are readable. This will in turn put you in the correct frame of mind to yield the fruits of TDD and will provide your team with concise documentation for your objects. Ensuring that a unit test clearly states a behavior of an object is an important step to truly benefiting from TDD.