What you need to do
The practical activities and techniques you need to master – what to practice, what to get good at, and what to keep doing.
Red-green-refactor cycle
The core TDD loop: write a failing test (red), write the simplest code to pass it (green), then improve the structure without changing behavior (refactor). The key insight is that writing the test first forces you to think about the interface before the implementation – how the code will be used, not how it will work internally. Start each session by writing a list of test cases, then pick one and cycle through red-green-refactor. Keep each cycle under five minutes. The most common mistake is skipping the refactor step – without it, TDD produces a messy aggregation of code fragments rather than clean, well-structured software.
Spec-driven development
Starting from acceptance criteria rather than jumping straight to implementation. Behavior-Driven Development (BDD) formalizes this by expressing requirements as scenarios: Given some context, When an action happens, Then an expected outcome occurs. The point isn't the specific Gherkin syntax – it's the discipline of defining what 'done' looks like before writing code. A product builder writes acceptance criteria in the PRD, then translates them directly into test cases. Each test is a contract: if the acceptance criterion says 'user sees an error when submitting an empty form,' that's a test. Modern spec-driven development tools like Kiro use AI to generate code from specs, but the human skill remains the same – writing clear, testable requirements.
Unit testing
Testing the smallest possible working unit – a function, a method, a component – in isolation from everything else. Unit tests are fast (milliseconds each), deterministic (same result every run), and laser-focused on business logic. A good unit test follows the given/when/then structure: set up preconditions, execute the action, verify the result. Name tests descriptively – 'returns error when email is invalid' tells you exactly what broke when it fails. Keep unit tests DAMP (Descriptive And Meaningful Phrases) rather than DRY – some repetition in tests is fine if it makes them readable. Unit tests form the base of the testing pyramid: many small, fast tests that cover individual logic paths.
Integration testing
Testing how components work together – verifying that the connections between modules, services, and external systems behave correctly. Integration tests catch issues that unit tests structurally cannot: database transactions, API contract mismatches, serialization bugs, authentication flows. Kent C. Dodds popularized the 'testing trophy' model, arguing that integration tests provide the best return on investment because they give high confidence without the brittleness of end-to-end tests or the narrowness of unit tests. 'Write tests. Not too many. Mostly integration.' The practical rule: if your test touches a database, makes a network call, reads files, or tests deployed binaries, it's an integration test. Stop mocking so much – when you mock something, you remove all confidence in the integration between what you're testing and what's being mocked.
End-to-end testing
Simulating real user flows across the entire application stack – from browser interaction through API to database and back. Playwright is the modern standard: cross-browser support (Chromium, Firefox, WebKit), built-in auto-waiting (no more flaky sleep statements), and a test generator that records your actions and produces test code. Key practices: each test should be completely isolated with its own state, use resilient selectors (getByRole, data-testid) instead of CSS classes that break when designers change styles, and use API calls to seed test data rather than clicking through UI setup flows. E2E tests are slow and expensive but give the highest confidence that your product actually works. Run them in CI, not just locally.
Mocking and test doubles
Replacing real dependencies with controlled substitutes to isolate what you're testing. Martin Fowler's taxonomy defines five types: dummies (fill parameter lists, never used), fakes (working but simplified implementations like in-memory databases), stubs (return canned answers), spies (record how they were called), and mocks (pre-programmed with expectations they verify). Google's engineering team learned the hard way that overusing mocking frameworks makes tests easy to write but painful to maintain – they rarely find real bugs. The pendulum has swung toward preferring fakes (real-ish implementations) over mocks (behavioral specifications). Use test doubles when you can't use the real thing (external APIs, credit card processing), but default to real implementations where feasible.
Code coverage analysis
Measuring which parts of your source code are executed during tests. Coverage types include line coverage, branch coverage (whether all conditional paths are tested), and function coverage. Martin Fowler's key insight: 'Test coverage is a useful tool for finding untested parts of a codebase. Test coverage is of little use as a numeric statement of how good your tests are.' High coverage with bad tests is worse than moderate coverage with thoughtful tests – you get false confidence. Use coverage tools to find blind spots, not to prove quality. Aim for the upper 80s or 90s naturally through good TDD practice. Be suspicious of 100% – it smells of someone writing tests to make numbers happy rather than thinking about what they're doing.
Code style and conventions
Enforcing consistent code quality through automated tools. ESLint catches bugs and enforces patterns (unused variables, missing error handling, unreachable code). Prettier handles formatting (spacing, line breaks, quotes) so developers never argue about style. TypeScript's strict mode catches type errors at compile time. Together they form the 'static analysis' layer of the testing trophy – bugs caught without writing a single test. Configure eslint-config-prettier to disable ESLint's formatting rules that conflict with Prettier. Use TypeScript's strict mode from day one – retrofitting it later is painful. These tools run in milliseconds and catch entire categories of bugs for free.
Git hooks and pre-commit checks
Automatically running quality checks before code enters the repository. Husky intercepts Git commands and executes scripts – most commonly a pre-commit hook that runs linting and formatting on staged files. lint-staged ensures these tools only run on files you've actually changed, not the entire codebase, keeping pre-commit times under a few seconds. The setup: Husky creates a .husky/ directory with hook scripts, lint-staged runs ESLint and Prettier on staged files, and any developer who installs project dependencies automatically gets the hooks configured. This catches problems at the earliest possible moment – before a commit exists – rather than failing in CI minutes later.
Continuous integration and deployment
Automating the entire path from code commit to production. Continuous Integration (CI) runs your test suite on every pull request – GitHub Actions is the standard for most teams. A typical pipeline: install dependencies, run static analysis (ESLint, TypeScript), run unit tests, run integration tests, build the application, run E2E tests against the build. Continuous Deployment (CD) extends this to automatically deploy successful builds. Deployment strategies include blue-green (two identical environments, instant switch), canary releases (roll out to a small percentage of users first), and rolling deployments (update servers gradually). Preview deployments from Vercel and Netlify give every pull request its own URL, letting reviewers see the actual running application before merging.
Frameworks to know
The mental models and structured approaches that guide how you think about this skill – not rules to follow, but lenses to see through.
Testing pyramid
Mike Cohn's classic model for structuring a test suite. Many fast unit tests at the base, fewer integration tests in the middle, and a small number of slow E2E tests at the top. The pyramid's insight: as you move up, tests get slower, more expensive, and harder to maintain – so have fewer of them. But they also give more confidence. The pyramid remains valid for large backend systems where unit tests catch complex business logic efficiently. The nuance modern teams add: the pyramid shows cost and speed, while the trophy shows confidence and ROI. Neither is universally 'correct' – the right balance depends on your architecture, team size, and what kind of bugs actually ship to production.
Testing trophy
Kent C. Dodds' alternative to the traditional testing pyramid. Where the pyramid puts unit tests at the base (most tests), the trophy puts integration tests at the center (most investment). The layers from bottom to top: static analysis (ESLint, TypeScript – free confidence), unit tests (isolated logic), integration tests (components working together – the sweet spot), and E2E tests (full user flows – few but critical). The reasoning: integration tests give the highest return on investment because they catch real-world bugs without the brittleness of E2E tests or the narrowness of unit tests. 'Write tests. Not too many. Mostly integration.' This model applies to individual codebases – microservices and distributed systems need additional strategies.
Arrange-Act-Assert (Given-When-Then)
The universal structure for writing readable tests. Arrange (Given): set up the preconditions – create objects, configure mocks, prepare test data. Act (When): execute the single action being tested – call the function, click the button, submit the form. Assert (Then): verify the expected outcome – check return values, verify state changes, confirm side effects. Each test should have exactly one reason to fail. If your test has multiple Act sections, split it into separate tests. This pattern applies identically to unit tests, integration tests, and E2E tests – only the scope changes. Naming tests after the behavior they verify ('displays error when email is invalid') makes failures self-documenting.
Good vs. bad
Common mistakes paired with what experienced practitioners do instead – and why the product builder approach changes the game.
TDD is the most misunderstood practice in software development. People hear 'write tests first' and think it's about testing. It's not – it's about design. When I write a failing test before writing the code, I'm forced to think about the interface: how will this function be called, what will it return, what should happen when things go wrong? That five minutes of thinking prevents hours of refactoring later. The tests are a byproduct. The real product is code that's designed to be used, not just designed to work.
The testing trophy changed how I think about where to invest testing effort. I used to write hundreds of tiny unit tests for every edge case, then wonder why bugs still escaped to production. The answer was obvious in hindsight: the bugs lived in the connections between components, not inside them. Now I write fewer unit tests (just the complex business logic), more integration tests (how components actually work together), and a handful of E2E tests for critical user flows. The total number of tests went down. The confidence went up.
AI has already transformed how I write tests. Not because AI writes perfect tests – it doesn't – but because it eliminates the tedious part. I describe the behavior I want to test, Claude or Copilot generates the scaffold, and I refine it. The human judgment about what's worth testing remains irreplaceable. But the boilerplate? That's gone. What used to take an hour of copy-paste-modify now takes ten minutes of review-and-adjust.
The biggest TDD anti-pattern I see in product teams isn't technical – it's organizational. Management mandates coverage numbers without understanding what coverage measures. Teams respond rationally: they write the cheapest possible tests to hit the number. Coverage goes up, quality doesn't. The fix is cultural, not technical. Stop tracking coverage as a KPI. Start tracking 'bugs that escaped to production' and 'time from commit to deploy.' If your tests are good, those numbers improve naturally.
Go deeper
Books to deepen your understanding beyond what fits on this page.
The foundational TDD text – two worked examples showing the red-green-refactor cycle in practice, from the creator of TDD and Extreme Programming.
The essential companion to TDD – the refactor step is where TDD produces good design, and this book is the definitive guide to doing it safely and systematically.
How to introduce tests into code that wasn't designed for them – techniques for finding seams, breaking dependencies, and gradually bringing untested code under control.
Practical guide to writing maintainable, readable, and trustworthy unit tests – covers test organization, naming conventions, and when to use different types of test doubles.
Explore related
Skills that complement this one – practice them together to build a stronger product builder toolkit.