HomeProjectsBlogContact

Test-Driven Development: A Practical Guide (Without the Dogma)

Write a failing test, make it pass, clean up. TDD sounds slow but often makes you faster. How the red-green-refactor loop works in practice, where it shines, and where it doesn't fit.

Test-Driven Development: A Practical Guide (Without the Dogma)

Test-driven development (TDD) has a reputation problem. Some people treat it as a religion; others dismiss it as a slow academic exercise. The reality is more useful: TDD is a tool that works brilliantly for certain kinds of code and adds little for others. Here's how to use it practically.

The loop: red, green, refactor

  1. Red: write a small test for behaviour that doesn't exist yet. Run it and watch it fail.
  2. Green: write the simplest code that makes the test pass.
  3. Refactor: clean up the code (and the test) while keeping everything green.

Then repeat with the next small piece of behaviour.

Watching the test fail first matters. It proves the test can actually detect the problem. A test that has never failed might be testing nothing.

A worked example

Say we need a function that builds a URL-friendly slug from a title.

Red. Start with the simplest case:

import { slugify } from './slugify'

test('lowercases and joins words with hyphens', () => {
  expect(slugify('Hello World')).toBe('hello-world')
})

It fails because slugify doesn't exist yet.

Green. The simplest thing that works:

export function slugify(text) {
  return text.toLowerCase().split(' ').join('-')
}

Next red. Add a case that breaks it:

test('removes punctuation and extra spaces', () => {
  expect(slugify('  Hello,   World!  ')).toBe('hello-world')
})

Green again:

export function slugify(text) {
  return text
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9\s-]/g, '')
    .replace(/[\s-]+/g, '-')
    .replace(/^-|-$/g, '')
}

Refactor if needed, and keep going with edge cases: empty strings, accented characters, numbers. Each test documents one rule about how slugs work. (Regex basics, for the curious.)

Why TDD often makes you faster

  • You design from the outside. Writing the test first forces you to think about how the code will be used: its inputs, outputs and name. That usually leads to simpler interfaces.
  • Tiny feedback loops. You find mistakes seconds after making them, not hours later in the browser.
  • Built-in regression safety. Every behaviour you added has a test, so refactoring is safe. (Clean code tips.)
  • Fewer debugging sessions. When something breaks, the failing test points straight at it.
  • Clear stopping point. When the tests for all required behaviour pass, you're done. No gold-plating.

Where TDD shines

  • Business logic: pricing rules, discounts, tax calculations, eligibility checks.
  • Parsing and transformation: dates, CSV imports, text formatting.
  • Bug fixes: write a test that reproduces the bug first. Then fix it. The bug can never quietly return.
  • Algorithms and utilities with clear inputs and outputs.
  • API endpoints, where you can describe the expected response for a given request. (Testing types.)

Where it's a poor fit

  • Exploratory work when you don't yet know what you're building. Spike first, then write tests once the shape is clear.
  • Visual design and layout, which are better checked by eye, screenshots or visual regression tools.
  • Thin glue code that only wires libraries together, where integration tests give more value.
  • Throwaway prototypes.

Being pragmatic about this is fine. TDD is a technique, not a moral test.

Tips that make TDD work

  • Take tiny steps. If a test takes 30 minutes to make pass, the step was too big. Split it.
  • One behaviour per test, named as a sentence: "rejects coupons that have expired."
  • Test behaviour, not internals, so refactoring doesn't break tests.
  • Keep tests fast. A suite that takes ten minutes won't be run every minute. Use unit tests for the inner loop and run slower suites in CI.
  • Run tests in watch mode so they re-run on every save.

The bug-fix habit (even if you never do full TDD)

If you take just one thing from TDD, take this: when you fix a bug, write a failing test for it first. It proves you understand the bug, proves the fix works and guarantees it stays fixed. It's the highest-value test you can write. (Debugging systematically.)

FAQ

What is test-driven development?

A practice where you write a failing test for the next small piece of behaviour, write just enough code to pass it, then refactor, repeating in short cycles.

Does TDD slow down development?

It can feel slower at first, but it often speeds things up overall by catching mistakes immediately, reducing debugging time and making refactoring safe.

Should I use TDD for everything?

No. It works best for logic with clear inputs and outputs and for bug fixes. Exploratory work, visual design and simple glue code are often better served by other testing approaches.

Keep reading

More from the workbench

All articles →