Test Driven Development : A Safety Net
The first time I learned about TDD, I thought, “Oh, God. Do we really have to do this? Why would you make a test that’s bound to fail? How does that make sense?” I was even surprised that TDD was considered as one of the best practices in programming. Confession, for quite some times, I do have some troubles writing codes for testing. I struggle to understand why we need to do this extra work. But these are the thoughts I have before I realize that it shapes me to write better codes rather than burdens me. How? We’ll get into it right now!
“If your code is not testable, then it is not a good design.” — Michael Feathers
Test-Driven Development and Its Rules
Test Driven Development is a development process that forms a cycle of testing codes and validating what it does. Basically, it’s a process of making tests for functionality, test them, then write a code to pass the test. In a nutshell, this is how a cycle of TDD goes :
- RED
First, we write a test for every small functionality in our program. “But we haven’t written anything. What’s there to test?” I know. That part might seem a little bit confusing — writing a test that’s bound to fail. But writing this test keeps you from writing a duplicated code or worse, unnecessary code. This is an example of a failed test before I made the code for last modified component.
- GREEN
After the test fails, we write a code that will help us pass the test. In my case, I need to write a code that can create the last-modified component. Thanks to the test earlier, I only write lines of codes that will be necessary.
- REFACTOR
After we pass the test, we could improve our code to make it more simpler and effective — this step is called refactor. It should be noted that when you refactor a code, it shouldn’t create a new functionality or make the changed code fails the test. The sole purpose of refactoring is to only increase the quality of your code. My code from the previous example was simple enough, so I don’t need to refactor it.
Unit Test vs Integration Test vs Functional Test
There are three types of tests, neither is less important than the others. Each holds an important role that may help developers and testers create a higher form of test.
- Unit test — A test that’s written from the programmer’s perspective to test a unit or the smallest piece of the program. It’s tested in isolation and has no dependencies. Unit tests takes the largest section of the pyramid, containing the solid parts of the program. They are easy to write, that’s why the cost is low. To write a unit test, we can use a technique called white box testing. In order to do white box testing, testers need to know the internal structure of the program. Documentation received from unit testing will be useful when we perform a functional test.
- Integration test — A test that’s written to test if different modules still work accordingly even after being combined with other modules.
- Functional test — A test that’s written from the user’s perspective in order to test the functionality of the system that may be dependent to other service such as database. Functional test is related to integration test and sometimes is considered the same thing. But functional test checks the entire application’s functionality while integration test only checks a few integrated modules of the system. To write a functional test, we can use a technique called black box testing. Unlike the white box, the tester doesn’t need to know the internal structure for the application as it is after all being written from the user’s perspective.
To know how much your tests have covered your codes, there’s a measurement called code coverage. It’s a powerful metric to measure the quality of your test suites. The higher the coverage the better. Having 80% coverage is considered good enough. But for this project, we aim to go higher than 90% coverage.
TDD and Clean Code
You might not expect it, but TDD has a relation to clean code. Writing bad tests is just the same as not writing one. As the application develops, the tests will perhaps need some modifications. Remember that we can refactor the tests and the code? If the tests are dirty, then it will be difficult for us to refactor or change them. Writing a code has its own guide, so does writing a test. We call it the FIRST principle :
- Fast — Tests should run fast. The longer a test runs, the lazier we will be to test it again. Not to mention, it will be a waste of time.
- Independent — Tests shouldn’t be dependent to any other test. This means, tests should be run in any order and won’t be affecting other test.
- Repeatable — Test should be able to be repeated in different environments. We have to make sure that the test doesn’t only pass locally, but also on another developer’s computer.
- Self-Validating — Test should only have two values : Pass or Fail. It’s tiring to look through the logs to see if the test has gone well.
- Timely — Test should be written before the actual implementation. If we’d written the implementation before the code, then it may take some time to write the test.
TDD and Its Benefit to The Team
In almost every article I read about TDD, it’s written that TDD helps improving the quality of the code. Though having to write test cases could consume time, it will save our energy for debugging as it will decrease the number of new bugs.
As for me, it helps me avoiding duplication of codes. Moreover, it saves my time from writing codes that will later be useless. TDD helps us stay focused on what needs to be done. Also, if (God-forbid) something happens to any member of the team and they only have written the test, the other member can simply pick up what the other member intends to do and what needs to be achieved.
It also increases our confidence toward change. Merging each other’s work will be troublesome if a conflict appears and we don’t know what’s failing it. But if we’ve done some tests and see that our tests have passed with a high coverage, then our fear to hit the merge button will become smaller. This is because TDD gives us a guarantee that our code has worked the way we planned it to be and should be safe to merge.
Practicing TDD might take some time, but it will be worth the while. After all, practice makes perfect!