TDD is Awesome You Should Try It! (Tutorial With Example)
Why TDD
TDD Test-Driven Development is a development methodology where tests are written before the implementation code.
- ✅ More efficient and purposeful development: The “expectation” is planned before writing code.
- ✅ Easier refactoring: Important parts have already been tested before refactoring the code, and if errors occur during the process, tests will indicate where the error happened.
- ✅ Code can be tested: Tests come before implementation code.
- ✅ Easier to achieve Atomic Commit: TDD makes it easier to draw clear boundaries between commits, ensuring that each commit does only one thing, as small as possible yet complete.
- ✅ Looser coupling: Easier to write loosely coupled code.
- ✅ Tests serve as documentation: Tests can help developers better describe the behavior of the code.
Legacy code is valuable code that you are afraid to change. Why are you afraid to change it? Because it is complex, undocumented, or hasn’t been touched in a long time. The purpose of TDD is to create a workflow that can verify the behavior of the code, allowing developers to refactor and modify the code with more confidence.
TDD Rules and Execution Process
-
Three rules:
- No product code should be written before writing a unit test.
- Only write unit tests that cannot pass, including those that cannot compile.
- Only write product code that can pass the current test.
-
Three execution processes:
- Red: Write a failing test.
- Green: Write the minimum code necessary to pass the test.
- Refactor: Review the code.
Practical Unit Test TDD: FizzBuzz
1. Preparation
- Development environment: Choose a familiar programming language and testing framework. Here, I use Vite TypeScript and Vitest as an example.
- Specification document: Simple version of FizzBuzz.
# FizzBuzz Specification Document:
1. Accept a number and output a string.2. If the input number is a multiple of 3, output "Fizz".3. If the input number is a multiple of 5, output "Buzz".4. If the input number is a multiple of both 3 and 5, output "FizzBuzz".
2. Red Light
Based on the first requirement: “Input a number and output a string,” we can start by writing a test and expect it to fail since our implementation code is still blank:
test('input number_output string', () => { expect(fizzbuzz(1)).toBe('1');});
// Implementation codeexport const fizzbuzz = (input) {};
3. Green Light
After writing the implementation code, we can expect the test for “input number_output string” to pass:
export const fizzbuzz = (input: number): string => { return input.toString();};
4. Refactor
When the green light is achieved, the product requirement is met. At this point, the code meets the requirement but may not be perfect. Refactoring can be done at this stage; if there is no need to refactor, the process of red light, green light, and refactor can be repeated.
5. Final Result
After going through the process of red light, green light, and refactor four times, the code is as follows:
// Implementation codeexport const fizzbuzz = (input: number): string => { // Initial version // const isFizz = input % 3 === 0 // const isBuzz = input % 5 === 0 // const isFizzBuzz = isFizz && isBuzz // if (isFizzBuzz) { // return 'FizzBuzz' // } // if (isFizz) { // return 'Fizz' // } // if (isBuzz) { // return 'Buzz' // } // return input.toString()
// After refactoring const fizz = input % 3 === 0 ? 'Fizz' : ''; const buzz = input % 5 === 0 ? 'Buzz' : ''; const output = `${fizz}${buzz}`; return output || input.toString();};
// Unit test codetest('Input number_Output string', () => { expect(fizzbuzz(1)).toBe('1');});
test('Input multiple of 3_Output Fizz', () => { expect(fizzbuzz(9)).toBe('Fizz');});
test('Input multiple of 5_Output Buzz', () => { expect(fizzbuzz(10)).toBe('Buzz');});
test('Input multiple of 3 and 5_Output FizzBuzz', () => { expect(fizzbuzz(15)).toBe('FizzBuzz');});
Summary
Having good automated tests as documentation can form a safety net for the code, ensuring that the behavior of the program is recorded and verified.
TDD Driven by Testing
Writing tests is not TDD; those who have actually practiced TDD can understand that its goal is not to write tests (tests are just a side effect) but to lead to good software through testing!
TDD Focuses on Testing Behavior (What) Rather Than Implementation Process (How)
Writing tests after implementing code can easily couple tests with implementation code. TDD forces us to examine requirements from a behavioral perspective, avoiding tests that focus on the implementation process.
Good Test Code Aligns with Good Code
Good test code possesses the same qualities: high cohesion, low coupling, separation of concerns… Programs developed through TDD naturally need to have these qualities.
Further Reading
- Test-Driven Development in JS with Acceptance Tests - Bran van der Meer
- Unit Testing Is The BARE MINIMUM - Continuous Delivery