In the previous article Test Driven Development (TDD): Example Walkthrough an example of TDD was given. It went from writing first test and its implementation to having a set of requirements fully tested and developed. Now it's time to learn what the best TDD practices are. This article will be built on examples from the previous one.
Best practices are solutions to a set of problems under certain situations. Not following them would make us "reinvent the wheel" and struggle with the same problems already solved by others. On the other hand, best practices should not be followed blindly. They should be tried out. With an informed decision at hand, they can be adopted as they are, modified to serve better specific situation or fully discarded.
Some of the best practices described here are inherited from other sets of practices and used when doing TDD. For example, most (if not all) unit testing practices should be used when doing TDD.
Article source code
Source code for all examples can be found in the github repository https://github.com/vfarcic/TechnologyConversations.git. Implementation is in the source directory src/main/java and tests can be found in src/test/java. Both are located in the package com.wordpress.technologyconversations.tddbestpractices. Code is written in Java and uses JUnit as the testing framework.
Before explaining best practices, it is important to understand the TDD life-cycle.
- Write the test
- Run the test (there is no implementation code, test does not pass)
- Write just enough implementation code to make the test pass
- Run all tests (tests pass)
Test-driven development is not about testing. Test-driven development is about development (and design), specifically improving the quality and design of code. The resulting unit tests are just an extremely useful by-product.
For more information, please read the Test-Driven Development (TDD) article.
TDD Best practices
Practices have been separated into following categories:
- Naming Conventions
- Development practices
Naming conventions help organize tests better so that it is easier for developers to find what they're looking for. Another benefit is that many tools expect that those conventions are followed. There are many naming conventions in use and those presented here are just a drop in the sea. The logic is that any naming convention is better than none. Most important is that everyone on the team knows what conventions are used and is comfortable with them. Choosing "more popular" conventions has the advantage that newcomers to the team can get up to speed fast since they can leverage existing knowledge to find their way around.
Separate the implementation from the test code
Benefits: avoids accidentally packaging tests together with production binaries; many build tools expect tests to be in a certain source directory.
Common practice is to have at least two source directories. Implementation code should be located in src/main/java and test code in src/test/java. In bigger projects number of source directories can increase but the separation between implementation and tests should remain.
The build.gradle can be found in the GitHub TechnologyConversations repository.
You'll notice that we are not specifying what to test nor what classes to use to create a jar file. Gradle assumes that tests are in src/test/java and that the implementation code that should be packaged to the jar file is in src/main/java.
Place test classes in the same package as implementation
Knowing that tests are in the same package as the code they test helps finding them faster. For example, examples in this article are in the package com.wordpress.technologyconversations.tddbestpractices. As stated in the previous practice, even though packages are the same, classes are in the separate source directories.
Name test classes in a similar fashion as classes they test
One commonly used practice is to name tests the same as implementation classes with suffix Test. If, for example, implementation class is StringCalculator, test class should be StringCalculatorTest.
Often, number of lines in test classes is bigger than number of lines in corresponding implementation class. There can be many test methods for each implementation method. To help locate methods that are tested, test classes can be split. For example, if StringCalculator has methods add and remove, there can be test classes StringCalculatorAddTest and StringCalculatorRemoveTest.
Use descriptive names for test methods
Benefits: helps understanding the objective of tests.
Using method names that describe tests is beneficial when trying to figure out why some test failed or when the coverage should be increased with more tests. It should be clear what conditions are set before the test, what actions are performed and what is the expected outcome.
There are many different ways to name test methods. Our prefered method is to name them using the Given/When/Then syntax used in BDD scenarios. Given describes (pre)conditions, When describes actions and Then describes the expected outcome. If some test does not have preconditions (usually set using @Before and @BeforeClass annotations), Given can be skipped.
An example of BDD format for naming test methods would be:
The whole class can be found in the GitHub TechnologyConversations repository.
Do NOT rely only on comments to provide information about test objective. Comments do not appear when tests are executed from your favorite IDE nor do they appear in reports generated by CI or build tools.
In the example screenshot, both failed tests have the same code inside. The only difference is in the name of the method. Test1 does not give much info regarding the failure. Method name whenSemicolonDelimiterIsSpecifiedThenItIsUsedToSeparateNumbers is much more descriptive and provides information even without going deeper into the log or the test code.
TDD processes are the core set of practices. Successful implementation of TDD depends on practices described in this section.
Write the test before writing the implementation code
Benefits: ensures that testable code is written; ensures that every line of code gets tests written for it.
By writing or modifying test first, developer is focused on requirements before starting to work on a code. This is the main difference when compared to writing tests after the implementation is done. Additional benefit is that with tests first we are avoiding the danger that tests work as quality checking instead of quality assurance.
Only write new code when test is failing
Benefits: confirms that the test does not work without the implementation
If tests are passing without the need to write or modify the implementation code then either the functionality is already implemented or test is defective. If new functionality is indeed missing then test always passes and is therefore useless. Test should fail for the expected reason. Even though there are no guarantees that test is verifying the right thing, with fail first and for the expected reason, confidence that verification is correct should be high.
Rerun all tests every time implementation code changes
Every time any part of the implementation code changes, all tests should be run. Ideally, tests are fast to execute and can be run by developer locally. Once code is submitted to version control, all tests should be run again to ensure that there was no problem due to code merges. This is specially important when more than one developer is working on the code. Continuous Integration tools like Jenkins, Hudson, Travis and Bamboo should be used to pull the code from the repository, compile it and run tests.
All tests should pass before new test is written
Benefits: focus is maintained on a small unit of work; implementation code is (almost) always in working conditions.
It is sometimes tempting to write multiple tests before the actual implementation. In other cases, developers ignore problems detected by existing tests and move towards new features. This should be avoided whenever possible. In most cases breaking this rule will only introduce technical debt that will need to be paid with interests. One of the goals of TDD is that the implementation code is (almost) always working as expected. Some projects, due to pressures to reach the delivery date or maintain the budget, break this rule and dedicate time to new features leaving fixing of the code associated with failed tests for later. Those projects usually end up postponing the inevitable.
Refactor only after all tests are passing
Benefits: refactoring is safe
If all implementation code that could be affected has tests and they are all passing, it is relatively safe to refactor. In most cases there is no need for new tests. Small modifications to existing tests should be enough. Expected outcome of refactoring is to have all tests passing both before and after the code is modified.
Practices listed in this section are focused on the best way to write tests.
Write the simplest code to pass the test
Benefits: ensures cleaner and clearer design; avoids unnecessary features
The idea is that the simpler the implementation the better and easier to maintain is the product. The idea adheres to the "keep it simple stupid" (KISS) principle. It states that most systems work best if they are kept simple rather than made complex; therefore simplicity should be a key goal in design and unnecessary complexity should be avoided.
Write assertions first, act later
Benefits: clarifies the purpose of the requirement and test early.
Once assertion is written, purpose of the test is clear and developer can concentrate on the code that will accomplish that assertion and, later on, on the actual implementation.
Minimize assertions in each test
Benefit: avoids assertion roulette; allows execution of more asserts.
If multiple assertions are used within one test method, it might be hard to tell which of them caused a test failure. This is especially common when tests are executed as part of continuous integration process. If the problem cannot be reproduced on a developer's machine (as may be the case if the problem is caused by environmental issues) fixing the problem may be difficult and time-consuming.
When one assert fails, execution of that test method stop. If there are other asserts in that method, they will not be run and information that can be used in debugging is lost.
Last but not least, having multiple asserts creates confusion about the objective of the test.
This practice does not mean that there should always be only one assert per test method. If there are other asserts that test the same logical condition or unit of functionality, they can be used within the same method.
This code contains 2 tests that clearly define what is the objective of those tests. By reading method name and looking at the assert it should be clear what is being tested.
This test has more than one assert but they are testing the same logical unit of functionality. First assert is confirming that exception exists and the second that its message is correct. When multiple asserts are used in one test method, they should all contain messages that explain the failure. This way debugging of the failed assert is easier. In case of one assert per test method, messages are welcome but not necessary since it should be clear from the method name what is the objective of the test.
This test has many asserts. It is unclear what is the functionality and if one of them fails it is unknown whether the rest would work or not. It might be hard to understand the failure when this test is executed through some of CI tools.
Do not introduce dependencies between tests
Benefits: tests work in any order independently whether all or only subset is run
Each test should be independent from others. Developers should be able to execute any individual test, set of tests or all of them. Often there is no guarantee that tests will be executed in any particular order. If there are dependencies between tests they might easily be broken with introduction of new tests.
Tests should run fast
If it takes a lot of time to run tests, developers will stop using them or run only a small subset related to the changes they are making. Benefit of fast tests, besides fostering their usage, is fast feedback. Sooner the problem is detected, easier it is to fix it. Knowledge about the code that produced the problem is still fresh. If developer already started working on a next feature while waiting for the completion of the execution of tests, he might decide to postpone fixing the problem until that new feature is developed. On the other hand, if he drops his current work to fix the bug, time is lost in context switching.
Benefits: reduced code dependency; faster tests execution.
Mocks are prerequisites for fast execution of tests and ability to concentrate on a single unit of functionality. By mocking dependencies external to the method that is being tested developer is able to focus on the task at hand without spending time to set them up. In case of bigger teams, those dependencies might not even be developed. Also, execution of tests without mocks tends to be slow. Good candidates for mocks are databases, other products, services, etc. Mock objects are a big topic and will be described in more details in a future article.
Use setup and tear-down methods
Benefits: allows setup and tear-down code to be executed before and after the class or each method.
In many cases some code needs to be executed before test class or before each method in a class. For that purpose JUnit has @BeforeClass and @Before annotations that should be used as the setup phase. @BeforeClass executes the associated method before the class is loaded (before first test method is run). @Before executes the associated method before each test is run. Both should be used when there are certain preconditions required by tests. Most common example is setting up test data in the (hopefully in-memory) database. On the opposite end are @After and @AfterClass annotations that should be used as tear-down phase. Their main purpose is to destroy data or state created during the setup phase or by tests themselves. As stated in one of the previous practices, each test should be independent from others. More over, no test should be affected by others. Tear-down phase helps maintaining the system as if no test was previously executed.
Do not use base classes
Benefits: test clarity.
Developers often approach test code in the same way as implementation. One of the common mistakes is to create base classes that are extended by tests. This practice avoids code duplication at the expense of tests clarity. When possible, base classes used for testing should be avoided or limited. Having to navigate from the test class to its parent, parent of the parent and so on in order to understand the logic behind tests introduces, often unnecessary, confusion. Tests clarity should more important than avoiding code duplication.
TDD, coding and testing in general are heavily dependent on other tools and processes. Some of the most important are following. Each of them is a too big of a topic to be explored in this article so they will be described only briefly.
Benefit: assurance that everything is tested.
Continuous integration (CI)
Use TDD together with BDD
Benefits: both developer unit test and functional customer facing tests are covered.
While TDD with unit tests is a great practice, in many cases it does not provide all the testing projects need. TDD is fast to develop, helps the design process and gives confidence through fast feedback. On the other hand, BDD is more suitable for integration and functional testing, provide better process for requirements gathering through narratives and is the better way of communication with clients through scenarios. Both should be used and together they provide the full process that involves all stakeholders and team members. TDD and BDD should be driving the development process. Recommendation is to use TDD for high "code coverage" and fast feedback and BDD as automated acceptance tests. While TDD is mostly oriented towards white-box, BDD often aims at black-box testing (more info on black-box vs white-box testing). Both TDD and BDD are trying to focus on quality assurance instead quality checking.
- Test Driven Development (TDD): Example Walkthrough
- Black-box vs White-box Testing
- Quality Assurance is not Quality Checking
- Code and Test Coverage
- Why do we need automation?
Test-Driven Java Development
Test-Driven Java Development book wrote by Alex Garcia and me has been published by Packt Publishing. It was a long, demanding, but very rewarding journey that resulted in a very comprehensive hands-on material for all Java developers interested in learning or improving their TDD skills.
If you liked this article I am sure that you'll find this book very useful. It contains extensive tutorials, guidelines and exercises for all Java developers eager to learn how to successfully apply TDD practices.