“I know I should write tests…
But I will just let my customers do that for me…
They should just report bugs”
Not a good idea, especially if your app only fails in some code that only gets executed by your boss 😜
So… Test first, code next!
Write your test first, then start coding. That is Test-Driven Development (TDD).
Developers should know that and should not need to follow that talk.
But if you don’t… Today, you’ll know 😉
Software testing is a basic build block in the software development process. It is responsible to identify bugs during software development, assure it behaves as it supposed, and increase the quality and the reliability of the product. There are many practices in software testing and Test-Driven Development is one of them.
In this post, we’re giving an overview of the Test-Driven Development (TDD) approach for software testing.
TDD VS. Traditional Testing
In general, the simplified ideal life cycle for a software feature looks like this:
- Implement it
- Test it
- Fix detected bugs
- You’re done!
The problem with this approach is that when the deadline is approaching and development is taking longer than planned, there are basically two options:
- Move the deadline
- Or cut testing activities.
Usually, under time pressure, a decision is made to drop some testing, deliver what is available (promising to fix eventually discovered bugs later) and hope for the best.
In the long term, this leads to the accumulation of bugs and technical debt. Eventually, the process would break down because new features become too difficult to implement. At this stage of the project, life is hell.
How test-driven development helps?
To avoid this kind of problems, test-driven development makes the following changes to the process:
- Split the process into many short micro-iterations.
- In each micro-iteration write test code before writing implementation code, make sure all tests pass and refactor “mercilessly” to keep the design malleable.
- Since the test code is written first and the objective is to make and keep the tests green at all times, the development is said to be test-driven.
- If a feature cannot be delivered under given time constraints, it will be naturally de-scoped. This is usually Ok since it is preferable to deliver some of the features with confidence that they are implemented correctly than trying to squeeze all the features in but failing to ensure consistent quality.
TDD Cycle (Red, Green, Refactor)
As we discussed in the previous section, TDD splits the development process into short micro-iterations (cycles). In this section, we’re having a deeper view of these micro-iterations.
Each micro-iteration consists of three phases: Reg, Green, and Refactor.
In the red phase, you have to write a test on a behavior that you are about to implement.
You act like you’re a demanding user who wants to use the code that’s about to be written.
You have to write a test that uses a piece of code as if it were already implemented.
Forget about the implementation!
If you are thinking about how you are going to write the code, you are doing it wrong!
Hence, this is the phase where you design how your code will be used by clients.
And this is the phase that makes TDD different from regular testing. Where you write a test so that you can then write code. You don’t write a test to test your code.
Best practices for this phase:
You have to make decisions about how the code will be used. You base this on what you really need at the moment and NOT on what you think may be needed.
So, do not write a bunch of functions/classes that you think you may need. Concentrate on the feature you are implementing and on what is really needed. Writing something the feature doesn’t require is over-engineering.
In this phase, you do what you usually used to do, you write code.
Best practices for this phase:
A big mistake common to be made here: instead of writing enough code to pass the red test, you write all the algorithms.
In this phase, you need to act like a programmer who has one simple task: write a straightforward solution that makes the test pass (and makes the alarming red on the test report becomes a friendly green).
You are allowed to violate best practices and even duplicate code. Code duplication will be removed in the refactor phase.
In the refactor phase, you are allowed to change the code, while keeping all tests green, so that it becomes better. What “better” means is up to you. But there is something mandatory: you have to remove code duplication.
What is the point of TDD? What does TDD bring? Why do we do TDD?
In fact, we’ve already given a part of the answer in the first section of this post, when we compare TDD and the traditional testing process. We saw how TDD let the development of production code and test code proceed in parallel, which results in that all production code is covered by automated tests. However, there are still other several reasons that push us towards following the TDD process:
1. To find the best design, by putting ourselves into the user’s shoes:
The best design is discovered by the one who has to use it.
By starting with “how do I want to use it” way of thinking, we discover the most useful and friendly design. Otherwise, what we get may be theoretically correct, but terribly hard to use.
2. To manage our fear:
TDD allows managing our fears, by giving us proof, that things work as they should. It gives us safety.
3. To have fast feedback:
How long can you code, without running the app? How long can you code without knowing whether your code works as you think it should?
Feedback in tests is absolutely important.
TDD practitioners reported a number of pitfalls related to it, and here we are going through them.
Despite the tests high coverage, there may be a large number of defects that may not be discovered by tests and surfaced only after deployment to production. This is obviously a problem since the ultimate goal of testing is to detect defects early!
Furthermore, the code quality may not increase as much as a person may expect.
Drilling down on the actual causes of this, there are three discovered patterns:
1. Invalid assumptions
A situation where there is no clear understanding and specification of the intended behavior of the units under test.
The tests would be constructed based on assumptions that simply may not hold under real production conditions e.g. assuming that the input data to the system would be of higher quality than was actually the case.
2. Ineffective tests
During testing, the units under test may exhibit erroneous behavior, but the tests may be not able to detect it.
This indicates that having a high level of test coverage alone does not imply the effectiveness of the test suite. Furthermore, it becomes evident that designing effective tests is indeed a challenging discipline, and requires the same level of care and thought as developing production code.
3. Treating test code differently
This is perhaps the biggest problem of all. When writing tests, the test code often designed differently than the production code. Best practices usually applied to implementation design maybe not applied rigorously to test code. One possible explanation for this is the idea that contrary to production code, the test code will only run during development and therefore it will never be exposed to real users.
However, this is short-sighted because the test code is an integral part of the whole codebase and needs to be evolved during the life-cycle of the project.
Pitfalls in The Design
In theory, test-driven development should have a positive impact on overall design quality. However, in practice, it has not always been the case. In some instances, automated tests even made it more difficult to improve the design. By drilling down on the problem, the Overhead effect can be identified:
Since the amount of available time is limited, the more effort a developer puts in creating test fixtures, the less time is left for exploration and evaluation of design options.
Frequently Asked Questions and Misconceptions about TDD
After all of this about TDD, some questions and misconceptions may jump to your head. Here we’re providing you with the answers by TDD practitioners.
TDD requires much more time than “normal” programming!
What requires time is mastering TDD as well as understanding how to set up and use a testing environment. When getting familiar with the testing tools and the TDD technique, it doesn’t require more time. On the contrary, it helps keep a project as simple as possible and thus saves time.
How many tests do I have to write?
The minimum amount that lets you write all the production code.
With TDD I don’t need to spend time on analysis and on designing the architecture.
If what you are going to implement is not well-designed, at a certain point this means that you will have to delete production and test code. It is true that TDD helps with the “Just enough, just in time” recommendation of agile techniques, but it is definitely not a substitution for the analysis/design phase.
Enhancing TDD with BDD
BDD – Behavioral-Driven Development.
The process is similar to TDD. In this also, the code is first written in BDD and then production code. But, the main difference is that in BDD the test is written in plain descriptive English type grammar, which is easy to understand by non-technical stakeholders also.
In this post, we introduced Test-driven development which is an integral technique for achieving high-quality software products. However, it is still a challenging discipline that takes time and practice to master. You need to pay attention to best practices, otherwise, you risk wasting time and not getting the expected benefits. Finally, this practice can only complement other design activities, not replace it.
Do you know that we use all this and other AI technologies in our app? Look at what you’re reading now applied in action. Try our Almeta News app. You can download it from Google Play or Apple’s App Store.