Test-Driven Development: Best Practices
There are several approaches to software development and one of such approaches is test-driven development (TDD). It is gaining popularity because, in contrast to the traditional development approach, it allows creating clear and maintainable code and, as a result, helps to deliver high-quality products.
What is TDD?
Test-driven development is a philosophy and best practice towards software development where the process starts with writing tests before the actual coding. The goal of TDD is to organize automatic testing of apps by writing functional, integration and unit tests that identify the requirements for the code before writing it. The implementation only provides as much functionality as it is required to pass the test and, thus, the tests act as a specification of what the code does.
How It Works
First, a written test examines whether the unwritten code works correctly. The test fails. After that, the developer writes the code that performs the actions required to pass the test. After the test is successfully passed, the developer might perform the refactoring of the written code under the control of the tests.
Successful implementation of TDD depends on the development processes described below.
- Write the test before writing the implementation code. This approach enables developers to focus on the requirements and helps to ensure that tests work as quality assurance, not quality checking.
- Write new code only when the test is failing. If tests don’t show the need to modify the implementation code it means either that the test is faulty or that the feature is already implemented. If there are no new features introduced then tests are always passed and therefore useless.
- Rerun all tests every time the implementation code changes. The way developers can ensure code modifications do not lead to unintended results. Tests should be run each time the implementation code is changed. After the code is submitted to version control, developers should perform all tests again to guarantee that no problem will arise due to code changes. This is particularly important when there is more than one developer involved in the project.
- Pass all tests before writing a new one. Sometimes developers ignore the problems revealed by existing tests and move towards new functionality. You may want to write several tests before the implementation actually takes place but it is better to resist the temptation. In most cases, this will lead to more problems.
- Refactor only after passing the tests. If the possibly affected implementation code passes all tests it can be refactored. In most cases, it doesn’t require new testing. Small changes to existing tests should be enough. The expected outcome of refactoring is to have all tests passed before and after the code has been changed.
TDD might seem time-consuming at the very beginning, however the developers are getting used to this approach over the time.
TDD Best Practices
Naming conventions help to better organize tests so that developers could quickly find what they are looking for. There are a multitude of naming conventions and the ones covered in this article are just a drop in the ocean.
- Use descriptive names for test methods. This method can help to find out why some tests failed or when the coverage should be expanded with more tests. It should be clear what conditions are set before the test, what actions are executed and what result is expected.
- Separate the implementation from the testing code. It is common to have at least two source directories. The implementation code is placed in src/main/java and the test code in src/test/java. The number of source directories may increase in large-scale projects but the separation between implementation and testing should be retained. The advantage of this practice is avoiding accidentally packaging tests together with production binaries.
- Place test classes in the same package as implementation. It is easier to find tests when they are located in the same package as the code they test.
- Name test classes in a similar way as implementation classes. Another practice is to name tests the same way as implementation classes with the suffix Test. The number of lines in test classes may be greater than the number of lines in the corresponding implementation class. Developers can divide test classes to help find methods that are being tested.
- Write the simplest code to pass the test. The simpler the implementation the better and easier the product maintenance. The idea follows the KISS (keep it stupid simple) rule that states that the majority of systems perform best if they are made simple. Hence, complexity should be avoided whenever possible.
- Write assertions first. Once the assertion is written, the purpose of the test is clear and developers can focus on the code that will execute that assertion and, later on, on the actual implementation.
- Minimize assertions in each test. If developers use several assertions within a single testing method, it may be difficult to determine which one of them has led to a test failure. This is particularly common when tests are carried out as a part of an ongoing integration process. It doesn’t mean that there should always be only one assertion per test method. If there are other assertions that test the same logical condition or unit of functionality, they can be used within the same method.
- Do not introduce dependencies between tests. Every test should be independent. Developers should be able to perform every test individually. There is no certainty that tests will be conducted in a specific order. If you create dependencies between tests you can easily break them by adding new tests.
- Tests should run fast. Running tests may take much time and sometimes developers either stop using them or run only small parts related to the changes they are making. Fast tests allow finding problems quicker and getting fast feedback. If a developer has already started building a new feature while waiting for the completion of testing, they may decide to postpone error rectification and get back to it after they create that new feature. As a result, it will take much longer to release a single feature.
- Use test mocks to enable TDD. Mocks enable developers to perform test execution faster, as well as focus on a single functional unit. Mocking dependencies external to the method being tested lets developers focus on tasks at hand without having to spend time setting them up.
- Do not use base classes. Developers sometimes approach code testing the same way as implementation. One popular mistake is to create base classes that are extended through tests. When de