Test-Driven Development
The red-to-green approach
by William Ventura, Senior Software Engineer at Growth Acceleration Partners
Overview
Well known as TDD, Test-Driven Development is the approach that allows developers to test their code programmatically. It means when you write a production code, you are required to attach and write some set of test cases where you do two things:
- Validate your feature is working correctly.
- Perpetuate a set of validations that should not be broken by any future changes by mistake.
This technique requires developers to write all the needed test cases for the production code, and forces them to clearly understand the problem before even starting to write the application code. This exercise will, as a result, produce robust, free of bugs, scalable and easy to maintain code.
The Process
The first step to having a clear idea of this methodology is to understand each phase of its lifecycle. See the following picture:
Everything starts with the fact that we need a production code. But to write it, we will need to follow the process described in the picture above.
- Create the set of tests that describe the behavior of the production code to be written.
- Write the production code that was described in the tests. Once you do that, you will pass all the tests.
- Look at your code carefully and see what you can improve to make it more efficient, scalable and atomic without altering the production code you described at the beginning.
There are only 3 steps in the process, but following the described process will unlock a wide range of benefits for your organization and your personal growth.
1. Describe your software using a testing tool
This forces you to describe the production code you really need in your organization. How? First, you should know that testing tools will allow you to do that just by writing your tests. In fact, testing tools allow developers to describe what the characteristics, behaviors, restrictions and business logic the software previously contemplated are. To better do this, you are forced first to understand the problem you are solving, because how can you describe something you do not know anything about? And also, this allows you to add important business rules other developers should not break when working with the codebase or adding new features in the future.
With this in mind, here is a list of the lessons learned in the first step of TDD:
a. Do not write any production code.
b. Testing tools allow developers to describe the code they want to write.
c. Developers should understand the problem clearly before even starting to write tests.
d. When writing tests, you should always consider the business rules that are essential for each process. If someone else breaks any of the rules, at least one tester should raise their voice and say, “Hey, you are breaking this characteristic, behavior or business logic in our source code, so please be careful.”
e. The source code in its entirety will be fully described through the tests written by the team.
f. Each test written in this step will fail because this behavior does not exist yet in the real world.
g. Learn how each testing tool works. You will need to be familiarized with mocks, assertions, exceptions, etc.
2. Turn caffeine into production code
The second step is a little bit simpler than the first one. This is all about writing the production code, and this should be easy to do because you already have the tests that will guide your work. Your goal in this phase is to turn the red failing tests into green passing tests. There are some key things to do while writing the production code in this step.
a. Write the production code.
b. Try to find a way to make code that works. How can you do this? Fit the described characteristics, behaviors and business logic you wrote in the first step.
c. Do not think about the best solution, best practice or best approach in this step.
d. The failing tests are the route that dictates how your code should work. Use that map, and land each test separately.
e. Do not waste your time testing things manually once you have progress in your task. Go ahead and run the test suite to see if you have progress on your task.
f. Use the testing features to run tests focused on what you are working on; sometimes testing a pipeline can take 15 minutes to check if all the tests pass or not. In this phase, learn how to run an isolated test or couple of tests.
g. You are done when red turns into green.
3. Refactor and improve your solution
As I said before, please do not focus on writing the best solution, applying best practices, or following the best approach; this phase is all about getting things done. You can ask several questions and refactor your code to fully support the answers to these questions:
a. How can I make this “production code that works” cleaner and easier to maintain?
b. Break complex features into small pieces of functionalities.
c. Review performance issues and solve them in this step.
d. Integrate the best practices on your solution, naming conventions, comments, documentation, design patterns, etc.
e. Think about how your production code can support the SOLID principles. This is not mandatory, but if you do this your code quality will improve a lot. Your solution will be easier to maintain and each feature easier to understand.
f. The last thing I will recommend is to support your team by thinking about what you can do to make this production code easy to understand for your team, in the review process, 1 week later, 1 month later, 2 years later, and so on.
How to Succeed in TDD
Now that you understand the process, I want to share some advice to make your journey easier when working with TDD.
- The more you can learn about testing tools, the more productive you will become using TDD. This approach requires the skill to write good tests. If you write poor test cases, your code quality will be very low.
- When you are creating your tickets on the board, you have to break complex problems into smaller features. It will help you properly cover the new production code with its tests.
- If a ticket has the definition of a model in the database, a complex algorithm for validations, and a new endpoint all working together, split it into three tickets with one covering database details, the other the complex algorithm, and the last one the endpoint.
- Continuously think about edge cases, and write tests for them in the very beginning. This is one of the main advantages of a company using this approach; your engineers will look after whatever weird behavior and will write code to fully support those edge cases. Your app will not break because, at the very beginning, a cool developer thoroughly thought of a solution for that unusual case.
- Learn about the SOLID principles; if you do this, you will have fun in the third step.
- When working with bugs, keep in mind the goal is not to silence the bug or associated logs. You should think all the time about the business and what is the impact of the decision you are making.
- Learn the types of tests you can write. At the very least, you have to understand when you are writing unit tests, integration tests, acceptance tests or end-to-end tests.
- Organize your tests in a way that you can quickly find where the associated unit test is located. A pattern I have seen pretty consistently across multiple repositories is to use a mirror between the source code and a folder named tests. Another pattern in javascript is to create a __test__ folder, and put there your tests associated with the components or files.
Test Types and Complexity
At this point, you will probably be wondering how many tests you should write for each solution. I will answer this question with the following well-known pyramid diagram:
Lets review them:
- Unit Tests: You will find yourself writing a big number of these tests, but they will be pretty simple as they test small pieces of functionalities. A red flag will be to invest a lot of time writing unit tests. You probably will need to learn more about how to write tests, or you are testing a complex feature. Such a feature should be broken into small pieces of functionalities. In this stage, you are not supposed to test complex features; you will only test atomic blocks of code.
- Component/Contract Tests: This kind of test is more time-consuming, but you will need to write a smaller number compared with unit tests. Here, we test more than one piece of functionality together. The precision of this kind of tests is bigger than unit tests’ precision, which means a failure in a test at this level can be an indicator that an important process will potentially fail in production. These kinds of tests do not involve database interaction.
- Integration Tests: These tests normally indicate a flow starting with an API call, and most of the time storing or retrieving data from the database. The cost of executing these tests is high, as they require an entire database to be available at the time the tests are being executed; but at the same time, they are more precise than component/contract tests. The number of tests will be smaller in correlation with the previous type.
- End-to-End (E2E) Tests: This is the most expensive test type, but you will write a smaller number of tests. For E2E, we just write tests for the core features or core workflows the company needs to keep its operations. A failure at this level of tests indicates that if the company releases this version, it will probably face an incident issue.
Conclusions
- There are three steps in the TDD approach: (1) write your tests describing the software you want; (2) write the source code described in the tests; and finally (3) refactor this production code to be more robust, efficient and easy to maintain.
- TDD forces you to understand the problem first and all the business rules associated with the processes the software supports. Using TDD will result in engineers who will be more aware of what is happening in the company, and they will understand each process better.
- There are 4 types of tests (Unit Tests, Component/Contract Tests, Integration Tests and E2E Tests), and we have at least three indicators that can be used as red flags when we are writing, running or even reviewing them: Test Quantity, Precision/Reliability/Speed and Cost/Time to Execute
- You should learn how to use testing tools; your code will be as good as your ability to write good tests.
- You will have fun and will increase code quality if you apply the SOLID principles in the third step of TDD. By doing so, your code will be clean, maintainable, easy to understand and decoupled.