█
LastWrite
  • > Curriculum
  • > Pricing
  • > For Educators
  • > About
  • > Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
LastWrite

Structured computer science lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 LastWrite. All rights reserved.
Curriculum/Software Engineering/Testing/Test-Driven Development
40 minIntermediate

Test-Driven Development

After this lesson, you will be able to: Run the red-green-refactor cycle and judge honestly when test-driven development helps and when it slows you down.

Test-driven development (TDD) flips the usual order: the test comes first. It is divisive because people treat it as a religion. This lesson teaches the actual mechanics, where it genuinely shines, and where writing the test right after is just as good.

Prerequisites:End-to-End Testing with Playwright

Red, green, refactor

The cycle has three steps repeated in tight loops. Red: write a small failing test for the next bit of behavior you want, and watch it fail (proving the test actually checks something). Green: write the minimum code to make it pass, even if it is ugly. Refactor: with the test now guarding you, clean up the code (and the test) without changing behavior. Then loop. The loops are small, minutes each, so you are never far from a known-good state.

A TDD loop in miniature

Build a roman-numeral function one failing test at a time.

tsx
// Step 1 RED: write the test first
it('converts 1 to I', () => {
expect(toRoman(1)).toBe('I');
});
// toRoman doesn't exist yet -> test fails (red)
// Step 2 GREEN: minimum code to pass
export function toRoman(n: number): string {
return 'I'; // yes, hard-coded. that's allowed in green.
}
// Step 3 RED again: add the next case
it('converts 2 to II', () => { expect(toRoman(2)).toBe('II'); });
// GREEN: now generalize just enough
export function toRoman(n: number): string {
return 'I'.repeat(n); // passes 1, 2, 3...
}
// RED: it('converts 4 to IV') forces the real algorithm.
// Each failing test pulls the implementation toward correctness.

When TDD helps and when it does not

TDD shines when the requirements are clear but the design is fuzzy: writing the test first forces you to design the interface from the caller's point of view before committing to an implementation. It also shines for bug fixes (write a failing test that reproduces the bug, then fix it, and you have a permanent regression guard). It helps least for exploratory or throwaway code, or when the design is already obvious, where writing the test immediately after is just as effective. TDD is a tool, not an identity. The goal is working, well-tested code, whichever order gets you there.

💡 The regression-test habit (do this even if you skip TDD)

Whatever your style, when you fix a bug, first write a test that fails because of the bug, then fix it. That single discipline turns every bug into a permanent guard and stops the same regression from shipping twice.

Quick Check

What is the point of watching the test FAIL before you write the code in TDD?

Pick the best answer.

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←End-to-End Testing with Playwright
Back to Testing
Running Tests in CI→