Test Driven Development: pros and cons
TDD or Test Driven Development is development through tests: first integration tests are written, in which the future features are described in detail. And new features are developed on the basis of these tests.
I’ve recently realized that I have been working on TDD for 5 years. The thing is that on my first projects TDD was used by default, so I had to get used to it and learn. That time TDD was familiar to everybody. However, in reality, very few people used it.
It's time to share experience when you need and don’t need to use TDD to get the most out of it.
What is it for
- Speeds up the development. While developers are getting used to writing tests, the work is going slower. When they’ve got used, the development speeds up;
- Functional tests high coverage. For each feature I write 20-25 tests to cover all possible options for its use;
- Less time for debugging. Debugging usually begins when the main features are already done. Here we debug the concept of the features beforehand while writing tests.
How it works
The path of any code in TDD goes through 3 stages:
- Red Stage: writing tests. The first test run always returns an error, because the interface that it tests isn’t ready. Each test must experience at least one fall throughout its existence. At least immediately after writing. As TDD followers say: "I can’t trust a test I've never seen fallen down." The fallen test will attract attention and make you think about the correctness of the features implementation or the test itself.
- Green stage: we develop features and re-test them. Our goal is to turn tests green. So we understand that the features were developed correctly. It is important that the test fell down at least once and was passed at least once too. It's easy to make a mistake and write a test that always falls or is always passed. But the test, which both fell and was passed, clearly tests some logic. After that we can assume that the functionality works correctly.
- Refactoring. We refactor a specific task, because we are already sure that we have a working test code. We need just to write a logical and clean code for our project.
Where it works
The choice of TDD depends on:
- Teams and the person who sets and accepts tasks (let's call him a customer).
- Internal processes on the project. For example, the team doesn’t use continuous integration. It isn’t worth using TDD here, because the developer won’t run tests every time before uploading the code to the repository. He will get lazy as the result.
There are criteria which can help you to determine that TDD will work for you:
- The project has a clearly described interface structure, for example, JSON-API or REST-API are used. Our task is to accelerate product development. When there is a clear structure, the developer doesn’t create the structure themselves. If there is no clarity, the developer creates the interface structure, and their vision may not coincide with the customer's vision. That’s why if the project has a well thought-out API structure, write tests before code;
- Various interfaсes have the same approach in implementation, which means that the logic and the construction order of the entities are the same or very similar. Before writing the tests, we will organize the required data in the test environment. If the project has a certain approach to the implementation of interfaces, you can safely apply one template and save time writing tests. If the development has no consistent approach, you will have to create your own conditions for each test before launching. This will take as much time as usual, so using TDD will not do you any good;
- The tasks are set in the following manner: one subtask is one test. The client who is also a technical specialist can set such tasks. Start the system like this: within the task, each subtask is one test. Then the developers just write tests that describe the subtasks, then the implementation, and the tests gradually become green. If it is possible to isolate several tests in each subtask, the process does not benefit in time compared to the usual development;
- There is a high-level test framework supporting integration tests for the technology used on the project. The alternative is a test framework on another technology that us separate from implementation. All my TDD are Ruby projects. Here I use the testing tools RSpec, Minitest, Capybara. Tests should be separated from implementation, so it does not matter which tool you choose. RSpec + Capybara can be used on any project that has an HTTP interface. It is better if such a tool is available in your language. So the developer does not have to switch context during development, which makes it faster;
- There are no separate testers in the team. If the team has testers, they should write the tests. As a result, the main advantage of TDD is lost - "the mental connection of the developer with the functionality", because the tests are written by another person. In order for TDD to benefit, a developer must write the tests.
TDD is a must if the developer writes an API that returns JSON to the frontend. If the developer writes an API without TDD, checking JSON manually takes a very long time. The longer the check, the more errors appear and it takes more time to fix them. When the tests are written, they automatically check JSON for errors. Pre-written tests will help the developer check JSON on the output. As a result, the development is faster.
It doesn’t work all the time
TDD is not the answer to all your questions and not a universal solution. It is suitable for some cases, but it can do a lot of harm in the others. If TDD is not suitable for the project, it will hinder and slow the development down. As a result, developers begin to believe that:
- TDD can only be applied to companies with their own IT products;
- We do not have a suitable stack of technologies for TDD;
- The methodology does not work;
- You can only do backend;
- It doesn’t work in our subject field.
To prevent this from happening, assess TDD before implementing it. Do not use it if:
- The tasks are set without proper elaboration. The task is set by a non-technical specialist who delegates the task of creating user stories to a developer. In such cases, writing tests in the beginning is a risk to spend more time on developing. For example, there is a task "we need a registration form. First, we need many fields, then accounts in social networks." The developer made a page on which the fields were located on top, and there were buttons of social networks at the bottom. It turned out that the customer wanted a three-step registration: 1 - surname, first name; 2 - accounts in social networks; 3 - additional information.
That's how a customer saw it
That's what a developer thought
You will have to to redo the features, and tests, which means double amount of unnecessary work;
- It is a better idea to write tests after the customer's approval of the functionality. Business logic hasn’t been developed in advance. The developer does not know exactly how the functionality will work before starting the development. I have worked with 3 BI systems for the customers’ internal needs. In all projects, their requirements changed daily. Customers did not always understand which metrics are needed, so the requirements were clarified after the development. The features need to be done as quickly as possible, so that the customer sees what is wrong, and suggests changes. If you try to write tests first, the customer will see the functionality later, and the terms will be constantly shifted;
- Complex calculations or large loads. If the project has complex calculations, the result of these calculations is difficult to predict in advance at the writing test stage. For example, you collect and analyze tons of user data to find out which services are more popular. We need the result in the tests, because the implementation will be compared with it.
To use or not to use TDD depends on the person responsible for the project and the values of the team. For many, writing tests before development means pushing the envelope. How so? In fact, there is nothing to worry about.
- The tests should cover not the code, but the functionality;
- Tests should be done separately from implementation;
- The tests must be executed in their own environment.
Therefore, TDD will help not only increase the awareness of the programmer's work (he also thinks about the functionality in advance and expects the tests to fail), but also to shorten development time.