TDD Best Practices
Use this skill when the main question is how to drive implementation from tests, how to structure a test suite, or how to recover discipline in a codebase where tests were written after the fact.
Working Style
- Write one failing test, then make it pass with the minimum code needed.
- Refactor only when all tests are green.
- Test behaviors, not implementations — the public API, not private methods.
- Keep tests as simple as the production code they verify.
- A test suite that is hard to change is as expensive as production code that is hard to change.
The Red-Green-Refactor Cycle
RED → Write a failing test that describes the next behavior.
GREEN → Write the minimum code to make it pass.
REFACTOR → Clean up both the code and the test — no new behavior.
The discipline is in the order. Never refactor on Red. On Green, add only the behavior demanded by the current failing test.
The 3 Laws (Uncle Bob)
- You may not write production code unless you have a failing unit test.
- You may not write more of a unit test than is sufficient to fail. An intentionally missing public API/type can be a valid first Red; unrelated compilation or setup failures are not.
- You may not write more production code than is sufficient to make the currently failing test pass.
Design Workflow
- Describe the behavior — what should the system do? Write the test name first.
- Write a failing test — make it fail for the right reason (assertion, not setup error).
- Make it pass — take the simplest path; you can clean up after.
- Refactor — remove duplication, improve names, reduce complexity.
- Repeat — the next test should be the smallest step forward.
Choosing Test Granularity
| Level |
Tests |
Speed |
Confidence |
| Unit |
Single class/function in isolation |
Milliseconds |
Behavior of one unit |
| Integration |
Multiple real collaborators |
Seconds |
Module boundaries work |
| Acceptance / E2E |
Full system from user perspective |
Minutes |
Feature works end-to-end |
Start with units for logic-heavy code. Start with acceptance tests when following outside-in TDD. Integration tests fill the seams.
Heuristics
When to mock
Mock or fake I/O boundaries in unit/application tests. Use real disposable infrastructure in adapter integration tests where SQL, collation, constraints, search analyzers, transactions, or protocol behavior are the subject.
When testing an Aggregate
Use real Value Objects and child Entities. Test commands through the root, including the rule that a rejected command leaves both state and pending events unchanged.
When testing Domain Events
Assert exact facts after Act: type/name, aggregate identity, business payload, count, and controlled metadata. Test subscriber handling separately from real-bus registration and prove durable duplicate/retry behavior with integration tests.
When testing a Value Object
Test the applicable public contract: semantic equality with distinct instances, exact invariant boundaries, and any exposed normalization, hashing, defensive-copy, operation, or serialization behavior. Inject clocks or policies instead of reading ambient context.
When a test is too big
Large setup is a signal to review fixture clarity and test scope, not proof that the test is too big. Split when multiple behaviors or failure reasons are coupled.
When tests break on every refactor
Tests are coupled to implementation, not behavior. Move the assertion to the public API surface.
When you can't write a test first
Treat difficulty writing a test first as a design or boundary signal; legacy constraints, framework coupling, or missing seams may require characterization tests before redesign.
Warning Signs
- Tests that only pass in a specific execution order.
- Mocks that reproduce the production logic (over-mocking).
- Tests with no assertion (
assertTrue(true)).
- Tests named after methods, not behaviors.
- A test suite that takes more than 10 minutes to run on CI.
- Tests that break when internal details change, not when behavior changes.
References
- Read
references/tdd-core-practices.md for Red-Green-Refactor detail, FIRST properties, the test pyramid, triangulation, and baby steps.
- Read
references/test-doubles.md for the Meszaros taxonomy (Dummy, Fake, Stub, Spy, Mock), when to use each, and the classicist vs mockist distinction.
- Read
references/tdd-schools.md for the London (outside-in/mockist) vs Chicago (inside-out/classicist) schools, BDD, and ATDD.
- Read
references/tdd-anti-patterns.md for James Carr's 15 anti-patterns, Ian Cooper's "TDD, Where Did It All Go Wrong" insights, and recovery strategies.
- Read
references/tdd-language-examples.md for Red-Green-Refactor walkthroughs in TypeScript, Java, Python, C#, Ruby, and PHP with their respective test frameworks.
- Read
references/projection-testing.md for duplicate, ordering, concurrency, checkpoint, schema migration, replay/rebuild, and eventual-consistency tests for read models.
- Read
references/migration-testing.md for transformation, dirty-data, idempotent rerun, batch, checkpoint, concurrent-write, reconciliation, cutover, and recovery tests for data migrations.
- Read
references/value-object-testing.md for semantic equality, invariant boundaries, deep immutability, normalization, optionality, serialization, and property-based Value Object tests.
- Read
references/aggregate-testing.md for invariant-first Aggregate tests, repository contract and adapter tests, including atomic failures, deterministic Mothers, collection equality, transaction propagation, and concurrency integration tests.
- Read
references/domain-event-testing.md for Aggregate event assertions, application handoff, subscriber and real-bus tests, Outbox/Inbox delivery tests, Integration Event contracts, and CDC mapping tests.
- Read
references/transaction-testing.md for atomic commit/rollback, connection propagation, concurrency, cleanup, commit ambiguity, decorators, and side-effect boundary tests.
- Read
references/cache-testing.md for cache hit/miss contracts, invalidation, TTL, key isolation, corruption, failure degradation, stampede, and HTTP privacy tests.
- Read
references/domain-error-testing.md for typed failure assertions, Result short-circuit tests, exhaustive boundary mappings, public redaction, unknown 500 behavior, Effect execution, and error-test-double caveats.
- Read
references/criteria-testing.md for parser/AST tests, shared query semantics, real-adapter operator/security tests, pagination traversal, joins, nested boolean filters, and false-confidence warnings.
Related Skills
- Use
oop-best-practices when a class is hard to test — the design needs improvement first.
- Use
refactoring-best-practices when adding tests to untested legacy code (characterization tests).
- Use
ddd-best-practices to decide Aggregate boundaries and invariant ownership before testing them.
- Use
data-migration-best-practices to design the operational migration workflow and cutover gates before testing it.
Source Influences
This skill is synthesized from:
- Test Driven Development: By Example by Kent Beck
- Growing Object-Oriented Software, Guided by Tests (GOOS) by Steve Freeman & Nat Pryce
- Working Effectively with Legacy Code by Michael Feathers
- xUnit Test Patterns by Gerard Meszaros
- Ian Cooper — "TDD, Where Did It All Go Wrong" (talk, Vimeo)
- James Carr — "TDD Anti-Patterns" (blog)
- Tim Ottinger & Jeff Langr — "Unit Tests Are FIRST" (Pragmatic Bookshelf)
- Dan North — "Introducing BDD"
- CodelyTV Aggregates course (including testing counterexamples)
- CodelyTV Value Objects course (including testing and TypeScript counterexamples)
- CodelyTV Repository Pattern course (including repository-double and integration-test counterexamples)
- CodelyTV Domain Events course (including self-asserting-double and Event Bus coverage counterexamples)
- CodelyTV Domain Modeling Errors course (including Result, Effect, stale-contract, and mock counterexamples)
- CodelyTV Criteria Pattern course (including converter, pagination, injection, and test-coverage counterexamples)
- CodelyTV Infrastructure Design Transactions course (including transaction-scope, shared-connection, deferred-side-effect, and coverage counterexamples)
- CodelyTV Infrastructure Design Cache course (including key-collision, invalidation, ETag, destructive-test, and self-asserting-double counterexamples)
- CodelyTV Four Rules of Simple Design course (including private-method coupling and passing repository-mock counterexamples)
1---2name: tdd-best-practices3description: Test-Driven Development guidance. Use when writing tests before implementation, applying Red-Green-Refactor, testing Value Object contracts, typed domain failures/Results, DDD Aggregates, Domain Events/subscribers, transaction or cache boundaries, data migrations/backfills, Criteria/query adapters, repository contracts or persistence adapters, choosing between test doubles, deciding test granularity, reviewing test suite quality, or diagnosing brittle tests.4license: MIT5---67# TDD Best Practices89Use this skill when the main question is how to drive implementation from tests, how to structure a test suite, or how to recover discipline in a codebase where tests were written after the fact.1011## Working Style12131. Write one failing test, then make it pass with the minimum code needed.142. Refactor only when all tests are green.153. Test behaviors, not implementations — the public API, not private methods.164. Keep tests as simple as the production code they verify.175. A test suite that is hard to change is as expensive as production code that is hard to change.1819## The Red-Green-Refactor Cycle2021```22RED → Write a failing test that describes the next behavior.23GREEN → Write the minimum code to make it pass.24REFACTOR → Clean up both the code and the test — no new behavior.25```2627The discipline is in the order. Never refactor on Red. On Green, add only the behavior demanded by the current failing test.2829### The 3 Laws (Uncle Bob)30311. You may not write production code unless you have a failing unit test.322. You may not write more of a unit test than is sufficient to fail. An intentionally missing public API/type can be a valid first Red; unrelated compilation or setup failures are not.333. You may not write more production code than is sufficient to make the currently failing test pass.3435## Design Workflow36371. **Describe the behavior** — what should the system do? Write the test name first.382. **Write a failing test** — make it fail for the right reason (assertion, not setup error).393. **Make it pass** — take the simplest path; you can clean up after.404. **Refactor** — remove duplication, improve names, reduce complexity.415. **Repeat** — the next test should be the smallest step forward.4243## Choosing Test Granularity4445| Level | Tests | Speed | Confidence |46|---|---|---|---|47| Unit | Single class/function in isolation | Milliseconds | Behavior of one unit |48| Integration | Multiple real collaborators | Seconds | Module boundaries work |49| Acceptance / E2E | Full system from user perspective | Minutes | Feature works end-to-end |5051Start with units for logic-heavy code. Start with acceptance tests when following outside-in TDD. Integration tests fill the seams.5253## Heuristics5455### When to mock56Mock or fake I/O boundaries in unit/application tests. Use real disposable infrastructure in adapter integration tests where SQL, collation, constraints, search analyzers, transactions, or protocol behavior are the subject.5758### When testing an Aggregate59Use real Value Objects and child Entities. Test commands through the root, including the rule that a rejected command leaves both state and pending events unchanged.6061### When testing Domain Events62Assert exact facts after Act: type/name, aggregate identity, business payload, count, and controlled metadata. Test subscriber handling separately from real-bus registration and prove durable duplicate/retry behavior with integration tests.6364### When testing a Value Object65Test the applicable public contract: semantic equality with distinct instances, exact invariant boundaries, and any exposed normalization, hashing, defensive-copy, operation, or serialization behavior. Inject clocks or policies instead of reading ambient context.6667### When a test is too big68Large setup is a signal to review fixture clarity and test scope, not proof that the test is too big. Split when multiple behaviors or failure reasons are coupled.6970### When tests break on every refactor71Tests are coupled to implementation, not behavior. Move the assertion to the public API surface.7273### When you can't write a test first74Treat difficulty writing a test first as a design or boundary signal; legacy constraints, framework coupling, or missing seams may require characterization tests before redesign.7576## Warning Signs7778- Tests that only pass in a specific execution order.79- Mocks that reproduce the production logic (over-mocking).80- Tests with no assertion (`assertTrue(true)`).81- Tests named after methods, not behaviors.82- A test suite that takes more than 10 minutes to run on CI.83- Tests that break when internal details change, not when behavior changes.8485## References8687- Read `references/tdd-core-practices.md` for Red-Green-Refactor detail, FIRST properties, the test pyramid, triangulation, and baby steps.88- Read `references/test-doubles.md` for the Meszaros taxonomy (Dummy, Fake, Stub, Spy, Mock), when to use each, and the classicist vs mockist distinction.89- Read `references/tdd-schools.md` for the London (outside-in/mockist) vs Chicago (inside-out/classicist) schools, BDD, and ATDD.90- Read `references/tdd-anti-patterns.md` for James Carr's 15 anti-patterns, Ian Cooper's "TDD, Where Did It All Go Wrong" insights, and recovery strategies.91- Read `references/tdd-language-examples.md` for Red-Green-Refactor walkthroughs in TypeScript, Java, Python, C#, Ruby, and PHP with their respective test frameworks.92- Read `references/projection-testing.md` for duplicate, ordering, concurrency, checkpoint, schema migration, replay/rebuild, and eventual-consistency tests for read models.93- Read `references/migration-testing.md` for transformation, dirty-data, idempotent rerun, batch, checkpoint, concurrent-write, reconciliation, cutover, and recovery tests for data migrations.94- Read `references/value-object-testing.md` for semantic equality, invariant boundaries, deep immutability, normalization, optionality, serialization, and property-based Value Object tests.95- Read `references/aggregate-testing.md` for invariant-first Aggregate tests, repository contract and adapter tests, including atomic failures, deterministic Mothers, collection equality, transaction propagation, and concurrency integration tests.96- Read `references/domain-event-testing.md` for Aggregate event assertions, application handoff, subscriber and real-bus tests, Outbox/Inbox delivery tests, Integration Event contracts, and CDC mapping tests.97- Read `references/transaction-testing.md` for atomic commit/rollback, connection propagation, concurrency, cleanup, commit ambiguity, decorators, and side-effect boundary tests.98- Read `references/cache-testing.md` for cache hit/miss contracts, invalidation, TTL, key isolation, corruption, failure degradation, stampede, and HTTP privacy tests.99- Read `references/domain-error-testing.md` for typed failure assertions, Result short-circuit tests, exhaustive boundary mappings, public redaction, unknown 500 behavior, Effect execution, and error-test-double caveats.100- Read `references/criteria-testing.md` for parser/AST tests, shared query semantics, real-adapter operator/security tests, pagination traversal, joins, nested boolean filters, and false-confidence warnings.101102## Related Skills103104- Use `oop-best-practices` when a class is hard to test — the design needs improvement first.105- Use `refactoring-best-practices` when adding tests to untested legacy code (characterization tests).106- Use `ddd-best-practices` to decide Aggregate boundaries and invariant ownership before testing them.107- Use `data-migration-best-practices` to design the operational migration workflow and cutover gates before testing it.108109## Source Influences110111This skill is synthesized from:112113- *Test Driven Development: By Example* by Kent Beck114- *Growing Object-Oriented Software, Guided by Tests* (GOOS) by Steve Freeman & Nat Pryce115- *Working Effectively with Legacy Code* by Michael Feathers116- *xUnit Test Patterns* by Gerard Meszaros117- Ian Cooper — "TDD, Where Did It All Go Wrong" (talk, Vimeo)118- James Carr — "TDD Anti-Patterns" (blog)119- Tim Ottinger & Jeff Langr — "Unit Tests Are FIRST" (Pragmatic Bookshelf)120- Dan North — "Introducing BDD"121- [CodelyTV Aggregates course](https://github.com/CodelyTV/aggregates-course) (including testing counterexamples)122- [CodelyTV Value Objects course](https://github.com/CodelyTV/value_objects-course) (including testing and TypeScript counterexamples)123- [CodelyTV Repository Pattern course](https://github.com/CodelyTV/repository_pattern-course) (including repository-double and integration-test counterexamples)124- [CodelyTV Domain Events course](https://github.com/CodelyTV/domain_modeling-domain_events-course) (including self-asserting-double and Event Bus coverage counterexamples)125- [CodelyTV Domain Modeling Errors course](https://github.com/CodelyTV/domain_modeling-errors-course) (including Result, Effect, stale-contract, and mock counterexamples)126- [CodelyTV Criteria Pattern course](https://github.com/CodelyTV/design_patterns-criteria-course) (including converter, pagination, injection, and test-coverage counterexamples)127- [CodelyTV Infrastructure Design Transactions course](https://github.com/CodelyTV/infrastructure_design-transactions-course) (including transaction-scope, shared-connection, deferred-side-effect, and coverage counterexamples)128- [CodelyTV Infrastructure Design Cache course](https://github.com/CodelyTV/infrastructure_design-cache-course) (including key-collision, invalidation, ETag, destructive-test, and self-asserting-double counterexamples)129- [CodelyTV Four Rules of Simple Design course](https://github.com/CodelyTV/four_rules_of_simple_design-course) (including private-method coupling and passing repository-mock counterexamples)