Need the #1 custom application developer in Brisbane?Click here →

Testing

Test-Driven Development

9 min readLast reviewed: March 2026

Test-Driven Development (TDD) is a methodology where you write tests before you write the code. It seems backwards at first—how can you test code that doesn't exist?—but it's powerful. TDD isn't just about preventing bugs; it's about designing better software. This page explains why TDD works and when to use it.

The TDD Cycle: Red-Green-Refactor

TDD follows a simple three-step cycle, repeated for every feature:

  1. Red: Write a failing test. The test describes the desired behavior, but the code doesn't exist yet, so it fails.
  2. Green: Write the minimal code to make the test pass. Don't optimize or over-engineer; just make the test pass.
  3. Refactor: Improve the code without changing its behavior. Make it cleaner, faster, more maintainable. The test still passes.

Then repeat for the next feature. The red-green-refactor cycle creates a rhythm: write test, make it pass, improve code, move to the next test. This process, done consistently, results in well-tested, well-designed code.

Why TDD Works

TDD produces better code because it forces you to think before you implement:

  • Clearer requirements: Writing a test forces you to define exactly what the code should do. Vague requirements become precise test cases.
  • Better design: To write a test, the function must be testable, which means it must have a clear interface and not depend on everything. Testable code is well-designed code.
  • Fewer dependencies: When you write tests first, you naturally design functions with fewer dependencies. Hard-to-test code (tightly coupled, global state) fails the test first and gets refactored.
  • Confidence to refactor: Once the test passes, you can refactor freely. The test guarantees you didn't break anything.
  • Documentation: Tests are living documentation. Someone reading your tests understands exactly how the code should behave.
  • Psychological benefits: Developers who use TDD report higher confidence in their code and less anxiety about deployment. The tests prove it works.
Note
TDD as design: Many developers don't realize TDD is as much about design as testing. Writing tests first is a form of thinking through the API before you build it. You're designing the function's interface by writing how you want to call it.

Writing a Failing Test First

The first step is writing a failing test. This describes the feature you want to build. For example, a checkout system needs to calculate the total price including tax.

Pseudocode test:

describe('calculateCheckoutTotal', () => {
  it('calculates total with tax', () => {
    const subtotal = 100
    const taxRate = 0.1 // 10% tax
    const result = calculateCheckoutTotal(subtotal, taxRate)
    expect(result).toBe(110) // 100 + 10
  })
}

This test doesn't exist yet. It fails. Now you write the minimal code to make it pass:

function calculateCheckoutTotal(subtotal, taxRate) {
  return subtotal + (subtotal * taxRate)
}

Now the test passes. You could refactor this (add comments, extract a variable), but it's already simple. Next, you write a test for another case (no tax, negative amounts, etc.) and repeat.

Behaviour-Driven Development (BDD)

BDD is TDD with a focus on behavior and readability. Instead of writing tests in code, you write scenarios in plain English (Gherkin syntax). This involves non-technical stakeholders (product managers, QA) in test writing.

Gherkin format:

Feature: Checkout total calculation
  Scenario: Subtotal with tax applied
    Given a subtotal of $100
    And a tax rate of 10%
    When I calculate the checkout total
    Then the result should be $110

These scenarios are executable (using tools like Cucumber). Non-technical team members can write scenarios; developers implement the code to make them pass. BDD bridges the gap between business requirements and code.

TDD for Different Types of Code

TDD works best for some types of code and less well for others:

CodeTypeWorkWellWorkLess
Business logic (calculations, algorithms)Excellent—tests are straightforward, clearly define requirements
Data validation, error handlingExcellent—many edge cases to test; TDD ensures coverage
Database interactionsGood—write test for query, implement query, test passes
UI componentsModerate—good for behavior, harder for visual designYou can test that a button calls a callback, but design decisions (colors, spacing) are harder to TDD
Complex algorithmsGood—write test for algorithm, verify it works
Experimental codeLess ideal—you don't know the requirements yetWhen exploring solutions, write code first, tests later

The pattern: TDD works when you know what you're building and can express it as tests. Exploratory code (research, prototypes) shouldn't be test-first. Write tests after you understand the problem.

Common Objections to TDD (and Responses)

"TDD slows down development. I can code faster without tests."

True in the very short term. Writing tests takes time. But bugs caught by tests save debugging time later. Studies show TDD developers are slightly slower initially but produce higher quality code with fewer bugs. Over the lifetime of a project, TDD saves time and sanity.

"I can't write tests for code that doesn't exist."

You can describe how you want to call it. That's what the test does. Test first, then implement. This forces you to think about the API before you code it.

"TDD is dogmatic. I don't always want to test first."

Fair. TDD is a tool, not a law. Use it for critical code (business logic, security-sensitive paths). Use it less for throwaway code or experiments. The best developers use TDD when it makes sense, not religiously.

"Our team doesn't have time to learn TDD."

True upfront. But TDD pays dividends. Start small: use TDD for the next new feature. Once your team feels the benefits, adoption grows naturally. It's an investment that pays off over time.

Tip
Start with tests, not necessarily TDD: If red-green-refactor feels forced, start with writing tests after code. But write them soon, before you move on to the next feature. Once you're comfortable, experiment with test-first. Many developers find they gradually shift toward test-first naturally.

TDD in Legacy Codebases

Legacy code (old code with no tests) is hard to test. You can't apply pure TDD to existing code. Instead:

  1. Add tests for new code: Going forward, write tests for anything new. This prevents the codebase from getting worse.
  2. Write tests when fixing bugs: When you fix a bug, write a test that catches it. This prevents regression.
  3. Gradually refactor with tests: When touching old code, add tests. Extract testable functions. Gradually improve the codebase.
  4. Characterization tests: Write tests that describe current behavior (even if it seems wrong). This documents the code and prevents accidental changes.

You can't rewrite legacy code overnight with TDD. But you can steadily improve it by applying TDD to new features and bug fixes.

TDD as Living Documentation

Tests written with TDD serve as documentation. They show exactly how the code should be used:

describe('UserRepository', () => {
  it('finds a user by ID', async () => {
    const repo = new UserRepository(database)
    const user = await repo.findById('user123')
    expect(user.email).toBe('alice@example.com')
  })

  it('returns null if user not found', async () => {
    const repo = new UserRepository(database)
    const user = await repo.findById('nonexistent')
    expect(user).toBeNull()
  })
}

A developer reading these tests immediately understands how to use UserRepository. What methods exist? What do they return? What happens for edge cases? The tests answer these questions. This is better than documentation that goes out of date; tests always match the code.

TDD in Different Team Sizes

Startups: Fast iteration is critical. TDD can feel slow. But as the codebase grows, technical debt accumulates quickly without tests. Many startups adopt TDD once they realize the cost of not testing. Start with critical paths; test everything eventually.

Mid-size teams: TDD shines here. Multiple developers working on the same codebase means tests prevent stepping on each other. TDD also reduces code reviews (tests prove the code works) and increases confidence in deployments.

Enterprises: TDD is essential. Large systems have many moving parts. Tests prevent catastrophic bugs. TDD also improves documentation and reduces the tribal knowledge problem (only one person understands how something works).

Agencies/Freelancers: TDD is harder in variable-scope work. But agencies that practice TDD deliver higher quality and have fewer bugs in production, which improves reputation.

Introducing TDD to a Team

If your team doesn't use TDD, introducing it gradually helps:

  1. Lead by example: Use TDD for your next feature. Show the quality and confidence it brings.
  2. Share learnings: When TDD catches a bug before it reaches production, mention it. Celebrate saves.
  3. Pair program: Work with teammates using TDD. Show them the workflow.
  4. Start with critical code: TDD for authentication, payments, data integrity. Less critical code can follow.
  5. Provide training: Workshops on writing testable code. Show frameworks and tools.
  6. Make it safe to fail: TDD is a skill. Developers will be slow at first. That's okay. Support them.
  7. Don't mandate it: Forced TDD creates resentment. Let teams adopt it when they see the value.
Developer Insight
TDD is not magic: It doesn't prevent all bugs. Code can be well-tested and still have logical errors. But it prevents many bugs and creates better design. Use TDD as one tool in your quality arsenal, alongside code reviews, static analysis, and manual testing.

Key Takeaways

TDD (Test-Driven Development) is a methodology where you write tests before code. The red-green-refactor cycle improves design and confidence. TDD works best for business logic and clear requirements. It's less ideal for exploratory code. TDD improves code quality and documentation. Start using TDD for critical code; expand gradually. Use BDD (Behaviour-Driven Development) to involve non-technical stakeholders. TDD is a skill that takes practice but pays dividends over the lifetime of a project.