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:

    1. No product code should be written before writing a unit test.
    2. Only write unit tests that cannot pass, including those that cannot compile.
    3. 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 code
export 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 code
export 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 code
test('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