TDD, A Savior or Nightmare?
Test-driven development is a programming methodology where the test is created before the coding. Below, I listed some reasons that made its application useful.
1. Test can act as the code’s blueprint: Before building a house, a blueprint is made to help housebuilders. Building tests help you understand how the app works. Further, it can help you create more optimized codes.
2. Help developers understand client requirements.
3. Maintainable codes, good for teamwork and help future developers.
4. Preventive, not only corrective: Building tests can make developers more aware of possible errors and edge-cases that may occur.
Three Phases of TDD
There are three core steps in TDD: Create test — Implement the Code — Refactor
1. Create the Test
This phase will be preluded with “[RED]” in the commit messages. Test script needs to be precise yet simple and reusable. For example, when you have a button, you can check if they redirect to the right path after being clicked. If later you add another one, you can reuse them. To begin with this step, you decide on the specific feature you want to implement. Make it specific and small.
Then, imagine how this feature works, what are the possible edge cases, what components will be used, and how they are going to interact with each other upon making the test script. You do not have to implement the whole functionalities at first. Test a small feature — implement — and iterate.
2. Implement the Code
This phase will be preluded with “[GREEN]” in the commit messages. Probably the most exciting part for you 😜. Developers will code the feature based on the written scenario. The aim is to remove the current errors from tests.
3. Refactor
This phase will be preluded with “[REFACTOR]” in the commit messages. In this phase, developers optimize their code by removing redundancy without changing the program’s behavior.
Types of Functional Testing
- Unit test: Check for the smallest unit, like functions or methods, to work properly. Cheap to automate.
- Integration test: Check if all modules used work well together. Example: API calls to the database.
- Acceptance Testing: Testing the end-to-end product before launch.
..and many more. So, are you still skeptical whether the initial hard work will be worth the time? Indeed, testing is not an easy concept. You need lots of practice to reap the most benefits. As TDD might look daunting at first, the following mentality can help you start.
- Start with a small feature.
- If you work on an individual project, be imaginative about the flow of the features. If you work along with PMs and other users, validate the product specifications efficiently.
In industry, roles of testing can be allocated to Software Engineer, QA Engineer, or SDET (Software Developer in Test). It depends on the company's policy.
How I Implemented TDD in My Project
Above is an example of a typical frontend unit test written in Jest. I created a test to check if the operator registration page is rendered correctly (lines 52–65). The syntax is like the following,
expect(/*get the component/).TestingFunction()
— to assert whether the testing behaves the way we want.
a. RED
Using the old commit message convention, I first created the failing test for my upcoming feature. Below, I created two tests to check 1) whether operator registration page renders correctly and, 2) whether operator homepage will render correctly when the right user is authenticated. The pipeline will fail because I haven’t implemented the feature.
b. GREEN
Then, I implemented the feature based on the test I created prior. Below is a code snippet that implements the registration page for operator. I only added a new route and add role as a new parameter to the component, so the registration page with rendered accordingly.
c. REFACTOR
In refactor stage, I fixed my code by removing redundant code blocks.
After features are implemented, you should check the test coverage. Test coverage is a technique to measure how many portions of our application code are tested. The higher the number, the more cases and functions are tested. Thus, 100% is a perfect score.
After running npm test ( that executes react-scripts test ), the report like above is given. How to interpret the reported numbers is as follows:
- Statement: Has each statement in the program been executed?
- Branch: In conditional statements like if/else, branches of scenarios are created. So, has each branch in the program been tested?
- Funcs: Has each function in the program been called?
- Lines: How many lines of codes are tested?
The above error shows that the sum of two numbers given should be 8, but 7 is received. This kind of error is what we’re gonna see back and forth to check whether our feature implementation has been correct.
My Thoughts
TDD is a practice applied in most companies that have shown some long-term benefits. Learning to code is not a one-off activity, you will also need to maintain it. One way to ease you (and others) maintain the code is by using TDD.
Advantages - How TDD Improved our Team Performance
- Simpler Code. Since we need to create the test first, we cannot arbitrarily code the implementation. Developers become more thoughtful to create a good feature flow because the test also acts as a blueprint.
- Maintainable. If we have another feature on backlog, we create the test first. The codebase becomes more modular and clean. It can save our time to debug.
- Tests can also act as documentation. They help our team to understand each other’s code better because the tests have an ordered scenario of how the feature works.
Disadvantages
- Slower process. Development might be slower because we need to make tests first, but it tradeoffs with less time to debug.
- Not 100% guaranteed. Especially when the coverage is not high. Sometimes, they don’t tell where exactly the error is.