Test-Driven Development
Practice Test-Driven Development - the red-green-refactor cycle, outside-in TDD, BDD with Cucumber/Gherkin, and making TDD sustainable in team workflows.
skill: Test-Driven Development category: Engineering tags: [TDD, BDD, Red-Green-Refactor, Outside-In TDD, Acceptance Testing] readingTime: 5
TDD is a design tool, not just a testing tool. Writing the test first forces you to think about the interface before the implementation - the test is the first client of your code. If writing the test is painful, the design is wrong. TDD doesn't slow you down; it eliminates the debugging sessions and late-discovered design mistakes that slow you down.
Workflow
- Master the red-green-refactor cycleThe three-step cycle creates a tight feedback loop. Red catches bugs early; green gets to working code fast; refactor improves design with confidence (tests guard against regression).
- Practice outside-in TDDOutside-in TDD (London School) starts from the outermost layer (acceptance test) and works inward through the layers. This ensures you only build what's needed and naturally discovers collaborator interfaces.
- Write behavior-driven development (BDD) specsBDD extends TDD with business-readable specifications (Gherkin). Non-technical stakeholders can read, validate, and contribute to specifications that also serve as automated tests.
- Use TDD for design feedbackPainful test setup is a design signal. If you need 10 lines of setup to test 3 lines of logic, the SUT (Subject Under Test) has too many dependencies or is doing too many things.
- Make TDD work in team environmentsTDD works best when the whole team practices it. Partial adoption creates friction - code without tests blocks TDD practitioners from refactoring safely.
Output Format
TDD: strict red-green-refactor (never write production without failing test), outside-in double-loop acceptance→unit, BDD Gherkin Feature/Scenario/Given-When-Then, design signals from test pain, and team TDD adoption via pairing + kata.
RED: write one failing test for one behavior
GREEN: minimum code to pass (even return hardcoded)
REFACTOR: improve design while tests stay green
Outside-in: acceptance test → unit test → implement → acceptance passes
BDD: Feature → Scenario → Given/When/Then → step definitionsExamples
Edge Cases & Gotchas
- TDD for database migrations - Database migrations are infrastructure changes, not business logic - test them with integration tests that check schema shape, not unit tests. TDD approach: write a test asserting the migration can run up and down.
- TDD for UI components - For React components, test rendered output and user interactions: render(component) → userEvent.click(button) → expect(screen.getByText("result")). Don't test DOM structure - test what the user sees and can do.
- Emerging design vs. up-front design - TDD produces emergent design - let the tests drive the structure. This is uncomfortable if you're used to design-first. Trust the process for the first few iterations; refactoring in step 3 is where the design emerges.
- TDD and type systems - In TypeScript, compiler errors count as "red" - a test that doesn't compile is still driving design. Write the test file, let TypeScript errors guide you to define the right types and interfaces.
When to Break These Rules
- Don't apply TDD to data-heavy exploration (EDA, ML experiments) where the "test" is your eyes on a plot. TDD shines for discrete behavior with clear inputs and outputs.
- Don't apply TDD rigidly to infrastructure-as-code (Terraform, Helm charts) - use integration tests (terratest, helm test) after writing, not before. The feedback cycle is too slow for TDD.
Reference Files
https://martinfowler.com/bliki/TestDrivenDevelopment.htmlhttps://cucumber.io/docs/bdd/