What is Test-Driven Development? Why TDD is so important?
When we search for a job we come across the “Test-Driven Development” term a lot but what does it mean? Why this feature is wanted that much by companies? What is the importance of TDD?
Before we talk about TDD, what does “Testing” mean anyway? In general, testing is finding out how well something works. In terms of human beings, testing tells what level of knowledge or skill has been acquired. Humans test other humans to measure them. We all are tested by someone until now, at least in our education life.
Let’s change our aspect to the developer aspect. Does our software need to be tested? Of course, it is. As a human, we can and we will do mistakes. Actually it is very inevitable. Even they are software, they are a product of a human in the end.
In the development phase, we only spend time and effort as an expense so developers always try to save time. It does not mean coding faster (speed is usually a bad thing), it means coding effectively and efficiently. Bugs, defects, errors, etc. cause time loss. Sometimes a developer spends days on a single bug fix. So for avoiding these we use TDD as a development methodology.
Software Testing aims to find bugs, defects, and any other problems of the development we develop. If we want to explain that in a terminological way:
Software Testing is the process of evaluating a system or its components with the intent to find whether it satisfies the specified requirements or not.
There are many test types that developers should do. Let’s explain them first, then jump into the TDD world.
There are many test types that are used to test the functionality of the software. Unit Testing, Regression Testing, Integration Testing, Smoke Testing, Acceptance Testing, End-to-End Testing, White Box Testing, Black Box Testing, etc. These tests are “functional tests” and there are also many “non-functional tests”. Every one of them has a different goal but many tests are created and executed by “testers”. Developers also in charge of some of the functional tests even you work with testers. Let’s check the most common 2 test types that developers should write: Unit Testing and Integration Testing.
Unit Testing aims to test units and components that are the smallest piece of code, in isolation of the system. We use test frameworks for doing Unit Testing. Every language has its own frameworks. For Java, there are JUnit, NUnit, TestNG, etc. These frameworks have their own methods for ease the testing in many ways. Tests should be written in a different folder than real code for isolation and the tidiness of structure.
But why do we need to do that in isolation? The answer is, by writing unit testing we want to check the component’s functionality, not its goal in the system. Let’s say we created a method that gets data from a database and we want to write a unit test for it. This unit test should check “Is that method capable to get data from a database?”, not “from our database?”.
The first question that appears in the mind is if we do not use our database which one we will use? Will we create a new one only for testing? There is a very cool feature named “Mocking” for this problem.
Mocking means creating a fake version of a service that can stand-in for the real one. Mocking helps us to test more quickly and more reliably. The most common way to mocking is adding mocking annotation “@Mock” a line above of we want to mock but there are many ways to do it. We can create a fake version of many things if we need them in Unit Testing.
Integration Testing aims to test the integration of units in other words, are they working without any problem together in the system we worked in. Integration tests are more complicated and more system-based than Unit tests.
Unlike Unit Testing, Integration Testing checks do units work properly with other units and do they do their jobs correctly or not. So if we want to give the same example, our Integration test should check “Is that method capable to get data from our database, and is it the data we expect?”.
Integration tests are created after Unit tests pass because it tests units that work together so firstly units should work properly. However, we do not need a completely developed system for writing an integration test. If we want to test 2 units that integrated so we just need these 2 units that developed and work properly.
We gain many advantages by creating Integration tests.
- If we miss some errors, bugs, and defects in the Unit Testing phase, we can detect them with Integration tests.
- Units can be developed by different individuals so they may have different aspects or logic than others. So units’ work principles can have some conflicts and differences and these can cause some errors. We detect these with Integration tests.
- Connection of interfaces and databases are tested with Integration Tests. Unit Tests uses “mocking” so they cannot do that.
There are some approaches for Integration Testing.
- Big Bang Approach is Integration Testing where all or most of the units are combined and tested at the same time.
- Top-Down Approach is an approach to Integration Testing where top-level units are tested first and lower-level units are tested step by step after that. If top-level units are developed first the in development phase, usually this approach is used.
- Bottom-Up Approach is an approach to Integration Testing where low-level units are tested first and top-level units step by step after that. If low-level units are developed first the in development phase, usually this approach is used.
- Sandwich (Hybrid) Approach is an approach to Integration Testing which is a combination of Top-Down and Bottom-Up Approaches.
Developers usually are responsible for Unit Testing and Integration Testing. There are also System Testing and Acceptance Testing they might involve but let’s get back to our main topic: What is Test-Driven Development?
For testing, we need a target. If we want to test a student, firstly we need a student. Otherwise, there is no meaning of testing. So, if we want to create Unit Tests, we obviously need units, and if we want to create Integration Tests, we need integrated units. This is the general logic of testing but TDD works differently.
Test-Driven Development works differently than the standard testing logic. In TDD we test first, code later. But as we said above, there is no meaning of testing without a target. Whatever we write, it will fail because there is nothing to test, right? Actually, that is what we want.
In TDD, we create a “failure test”. That failure test is intended to fail and it covers all goals of whatever we want to test. Then we code to correct these tests and create our units. With that,
- we can see clearly what the unit should achieve.
- we can code with safer steps.
- we can detect bugs and their causes when we code.
- we can reduce the cost of a moderate increase in the development effort.
- we can create a code with no repetition (DRY— Don’t Repeat Yourself principle).
- we can avoid writing unnecessary codes because each code unit is preceded by a test describing its functionality (YAGNI — You Aren’t Gonna Need It principle).
TDD is a very helpful methodology the developers who seek cleaner and less bugged code development. There is one Golden Rule of TDD methodology:
Never write new functionality without failing test.
We can also say like “Whatever we create, test first then code.”. Unless we do not break this golden rule, we can create any functionality.
TDD works in a loop and there are some phases we need to follow. These are:
1. Writing Failure Tests
Write automated tests that will initially fail for the new functionality being implemented.
As we mentioned above, these tests should reflect the functionality of units that we created and they must fail. After we created all tests, we should move to the next phase.
2. Writing “Just Enough” Code
Write the minimum code needed to make the test pass.
We are not trying to code perfectly here, “just enough” code is ok for us. In the next, step we will finish our units. After we make all tests pass, we should start Refactoring.
Clean up the code or add additional use cases to the test.
Refactoring means restructuring the source code to improve operation without altering functionality. After we wrote “just enough” code for passing tests, we should refactor them to make them more perfect and if needed we should add some codes. We should also delete unnecessary lines and methods, we should correct indentations, etc. in brief, we should clean our code.
4. Re-Checking the Tests
Ensure the tests still pass.
We may break some codes in the Refactoring phase. We should recheck tests after refactoring, and if we see some failing tests, we must fix that error and refactor again.
Repeat the loop for new units and functionalities.
TDD is worked as a loop until we finish all the applications. After all phases are done, we can create a new test for new functionalities.
Some Clarifications About Test-Driven Development
- TDD is not a Testing method or not a Design method. It is a Developing Methodology.
- We usually hear TDD in Unit Testing but it is not limited to Unit Testing only. We should not forget what the golden rule says. If we use TDD, whenever we create any kind of functionality, we should write the test cases first. So, TDD is a methodology that contains Integration Tests, System Tests, and Acceptance Tests along with Unit Tests if we are adding new functionality.
- There can be some misunderstanding about the “just enough” code term. In TDD, we are not creating a system that barely passes the tests. Firstly, we write the minimum amount of code to pass tests, then we build our application over that structure.
- TDD doesn’t mean we need to write lots of tests. We should not overdo this part and spend a lot of time. TDD aims to decrease development effort so we might be careful so spending so much time on the testing phase because it is very against its nature.
Test-Driven Development is becoming more popular year by year. Many developers use TDD because of its many benefits. They save time and effort, gain confidence by using TDD, besides, they achieve cleaner and tidier codes. In the aspect of companies, they also gain advantages in an economic way too. Thanks to having a structure that we create with tests, in the beginning, our quality of codes also improves. If we want to gain these advantages, we should consider using Test-Driven Development.