RobinSinghAI Engineer · Interface
← Writing
Engineering30 May 2026·10 min read·Robin Singh

My Experience with Test-Driven Development

A practical, point-by-point guide to how I use TDD in real projects: what it is, why it helps, and how to start without overthinking.

test-driven developmentTDDunit testingred green refactorcode qualityhow to start TDD
01

Why I changed how I write code

Software is never truly finished. Requirements evolve, edge cases appear, and features that looked complete yesterday break after one new change today. I used to write code first and tests later (or never). That usually led to long QA loops, regressions, and uncertainty while refactoring.

TDD did not remove complexity. It gave me a way to control it.
Robin Singh
02

What TDD actually means (in simple words)

Core idea
  • 1) Write a test for a small behavior before writing implementation.
  • 2) Run tests and watch it fail (Red).
  • 3) Write the minimum code to pass the test (Green).
  • 4) Clean up the code while keeping tests green (Refactor).
Fig. 1 — the loop
Step 01
Red
Write the test. Watch it fail for the right reason.
Step 02
Green
Write the minimum code that makes it pass.
Step 03
Refactor
Clean the code while the tests stay green.
↺ repeat, 2–10 minutes per cycle

This cycle forces clarity. Before coding, I have to decide what success looks like. That one habit alone improved how I design functions, APIs, and components.

03

Why this helped me in real projects

Practical benefits I noticed
  • Faster debugging: failures point directly to broken behavior.
  • Safer refactors: I can restructure code without guessing what might break.
  • Better architecture: logic naturally moves into testable units instead of bloated UI files.
  • Less QA ping-pong: many regressions are caught before the ticket goes for review.
  • Higher ownership: if I break something, I catch and fix it immediately.
Re-opened tickets
Confidence to refactor
lower
Time spent firefighting
04

Red → Green → Refactor example

A tiny example shows the flow better than theory. First, define behavior through tests. Then implement just enough logic to satisfy them.

tsred → green
import { describe, it, expect } from 'vitest'

// RED: define behavior first
describe('calculateDiscount', () => {
  it('returns 10% discount for premium users', () => {
    expect(calculateDiscount(200, 'premium')).toBe(20)
  })

  it('returns 0 for standard users', () => {
    expect(calculateDiscount(200, 'standard')).toBe(0)
  })
})

// GREEN: minimum code to pass tests
function calculateDiscount(amount: number, tier: 'premium' | 'standard') {
  if (tier === 'premium') return amount * 0.1
  return 0
}
05

A mistake I made (and how TDD fixed it)

In one feature, I mixed API calls, formatting rules, and UI state in the same component. Tests became fragile because network responses changed. I split the transformation logic into a pure function and tested that function independently. Result: simpler component, stable tests, and faster feature updates.

Anti-patterns I avoid now
  • Writing huge tests that verify too many things at once.
  • Mocking everything blindly instead of isolating one unit clearly.
  • Asserting implementation details instead of behavior.
  • Treating test count as quality. Reliable tests matter more than many tests.
06

How to start TDD without getting overwhelmed

Beginner-friendly starting plan
  • Pick one small pure function (formatting, validation, mapping).
  • Write 2-3 clear tests for expected behavior and edge cases.
  • Use naming like "should return X when Y" so intent is obvious.
  • Keep one test file close to the code it validates.
  • Run tests frequently; keep cycles short (2-10 minutes each).
07

Checklist for a good test

Before you commit
  • Is the test readable without opening implementation?
  • Does it fail for the right reason when behavior breaks?
  • Is it deterministic (no flaky network/time/random dependencies)?
  • Does it validate outcome, not internal private details?
Start small, stay consistent, and let tests guide design. TDD becomes powerful when it is a daily habit, not a one-time sprint ritual.
← ArchiveNext: Passing data between microservices →