Test-Driven Development flips the traditional coding process. Instead of writing code and then testing it, you write the test first, watch it fail, make it pass, then clean up. This simple inversion changes how you think about software design.
Red-Green-Refactor
The TDD cycle has three steps, repeated continuously:
- Red: Write a test for behavior that does not exist yet. Run it. It fails.
- Green: Write the minimum code needed to make the test pass. Nothing more.
- Refactor: Clean up the code — remove duplication, improve naming, simplify logic. Run the tests again to confirm nothing broke.
Each cycle takes minutes, not hours. You build functionality in small, verified increments.
A Complete Example
Let's build a formatPrice() function using TDD. Start with the test:
// formatPrice.test.ts
import { formatPrice } from './formatPrice';
test('formats whole dollar amounts', () => {
expect(formatPrice(10)).toBe('$10.00');
});Run the test — it fails because formatPrice does not exist yet. That is the red phase.
Now write the minimum code to pass:
// formatPrice.ts
export function formatPrice(amount: number): string {
return `$${amount.toFixed(2)}`;
}Run the test — it passes. That is the green phase. Now add the next test:
test('formats amounts with cents', () => {
expect(formatPrice(9.99)).toBe('$9.99');
});
test('formats zero', () => {
expect(formatPrice(0)).toBe('$0.00');
});Both pass with the existing implementation. Add a harder case:
test('adds comma separators for thousands', () => {
expect(formatPrice(1499.99)).toBe('$1,499.99');
});This test fails — the current implementation does not handle commas. Fix it:
export function formatPrice(amount: number): string {
return '$' + amount.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}All tests pass. Now refactor if needed — in this case, the code is already clean.
Why TDD Works
TDD provides several benefits that are hard to get from writing tests after the fact:
Forces Clear Requirements
Writing a test first forces you to think about what the code should do before thinking about how. You define the inputs, outputs, and edge cases upfront. This catches ambiguity early — before you have written hundreds of lines of code built on a wrong assumption.
Produces Better Design
Code written to pass tests tends to be more modular. If a function is hard to test, it usually means the function does too much. TDD naturally pushes you toward smaller, focused functions with clear interfaces.
Eliminates Fear of Refactoring
When every behavior has a test, you can refactor with confidence. Change the internal implementation, run the tests, and know immediately if you broke something. Without tests, developers avoid refactoring — and the code slowly rots.
Acts as Documentation
Tests describe what the code does in concrete, executable terms. When you come back to a codebase months later, the tests tell you exactly what each function is expected to do, including edge cases.
Trade-Offs and When TDD Shines
TDD is not a universal rule. It works best in certain situations:
TDD shines when:
- Building business logic (calculations, validations, transformations)
- Working with well-understood requirements
- Maintaining code that changes frequently
- Writing libraries or APIs with defined contracts
TDD is harder when:
- Prototyping or exploring ideas where requirements are unknown
- Building UI layouts where visual verification matters more
- Working with external systems that are difficult to mock
- Writing one-off scripts that will not be maintained
Even if you do not follow strict TDD all the time, the discipline of thinking about tests first improves your code. Many developers practice "test-first" loosely — sketching out test cases before implementation, even if they do not follow the rigid red-green-refactor loop for every line.
Key Takeaways
- TDD follows a red-green-refactor cycle: fail, pass, clean up.
- Write the test before the code, then write the minimum code to pass.
- TDD forces clear thinking about requirements and produces modular, testable designs.
- Use TDD for business logic and well-defined APIs; relax it for exploratory work and UI layout.