TDD Workflow
Test-driven development produces better-designed, more maintainable code by writing tests before implementation. The critical skill is choosing the right test level — not defaulting to unit tests with mocks.
Core Philosophy: Right Level, Real Dependencies
- Default to the highest useful level. If a test can run against real infrastructure in under 5 seconds, it should. Don't mock what you can test for real.
- Mock only at boundaries you don't own. External APIs, email providers, payment gateways, third-party webhooks — mock these. Your own database, your own repositories, your own event bus, your own HTTP handlers — use the real thing.
- Test behavior, not mechanics.
verify(repo).save(any()) proves a method was called, not that the right thing was persisted. Assert on outcomes: query the database, check the HTTP response, verify the event payload. If you can only assert that a mock was invoked, you're testing wiring, not behavior.
- Parameterize repetitive patterns. If N endpoints x M roles all need the same auth check, write one parameterized test iterating all combinations. If 20 DTOs all validate
@NotBlank on their name field, test the validation framework once, not 20 times.
- Don't test the framework. The framework is already tested by its maintainers.
Test Level Selection
Choose the test level based on what the code does, not on convention or habit.
When to write integration tests (real DB, real HTTP, real infra)
- Persistence code — any repository, DAO, or data access layer. Custom
@Query methods, complex joins, constraint validation, cascades. These are the most bug-prone code in any project and the most dangerous to mock. Use Testcontainers, @DataJpaTest, in-memory databases, or test containers.
- API endpoints — controller/handler tests that verify request→response through the real routing, serialization, validation, and service layers. Use
@WebMvcTest/@SpringBootTest, supertest, httptest, TestClient, etc.
- Event-driven flows — listeners, propagation engines, schedulers. Test that publishing an event produces the right side effects in the real database.
- Migrations — schema changes tested against a real database to catch column mismatches, constraint violations, data loss.
- Multi-component interactions — anywhere two or more of your own components collaborate and mocking one would hide real bugs (transaction boundaries, error propagation, ordering).
When to write unit tests (isolated, fast, no I/O)
- Pure business logic — calculation engines, state machines, validation rules, parsers, formatters. These have clear inputs→outputs with no I/O.
- Domain model behavior — methods on entities/value objects that compute something.
- Algorithm-heavy code — sorting, filtering, transformation, scoring.
- Error path logic — retry strategies, circuit breakers, fallback chains (logic only, not the actual I/O).
When to write E2E tests
- Complete user journeys — multi-step flows from entry point to observable outcome (signup→verify→first use, create→classify→propagate).
- Cross-boundary flows — requests that traverse multiple services or modules.
- Critical business paths — the paths where a bug means revenue loss, compliance violation, or data corruption.
E2E tests are defined at the feature level by the planner (see Feature-Goal Tests in the plan), not per-task. Include a dedicated final-wave E2E task ONLY for features with a genuine multi-step cross-boundary journey; skip it for single-component changes, pure-logic changes, and bugfixes (per-task integration tests already cover that surface). When present, it runs after all implementation is merged.
Test Depth
A per-feature dial (from .claude/devline.local.md test_depth, or inferred during brainstorm). Two levels:
- deep — exhaustive: a unit test per method plus edge cases and all configs, plus integration and E2E. This is the current default thoroughness.
- focused — big behavior tests over whole classes/workflows plus targeted tests for genuinely hard logic; integration/E2E for real journeys; SKIP exhaustive per-method unit tests for trivial code (getters, passthroughs, obvious branches).
Acceptance criteria as tests
The brainstorm defines behavioral acceptance criteria. Each criterion becomes ONE behavior/workflow-level test, named to read as the criterion — the test name IS the spec sentence. There are no durable spec docs; the committed tests ARE the living spec.
Under focused, these acceptance tests are the primary suite: do NOT write a unit test per method for trivial code — only for genuinely hard or edge logic. Under deep, the acceptance tests sit on top of the exhaustive per-method units.
focused changes nothing about level selection: per-test [unit]/[integration]/[e2e] tagging stays, NEW I/O (persistence, endpoints, events) is still [integration] by default, the E2E task stays SHOULD (only for genuine cross-boundary journeys), and the integration size-gate — a targeted unit test suffices when modifying existing integration-tested code without changing its schema/contract — still applies. focused only drops the redundant per-method units for trivial code; it never downgrades a real journey or a new I/O surface.
What NOT to Test
These produce noise without catching bugs. Delete them if they exist; don't write new ones.
- Framework behavior —
@NotBlank rejects blank strings, @Valid triggers validation, Spring Security enforces @PreAuthorize. The framework is tested by its own test suite. Only test YOUR validation logic (custom validators, conditional rules, business constraints).
- Language features — data class defaults, null safety, enum values, constructor parameters. If Kotlin/Java/TS guarantees it, don't test it.
- Delegation methods — one-liner passthrough methods that just call another method. Testing
helper.doThing() calls service.doThing() verifies nothing useful.
- Trivial getters/setters — unless they contain logic.
- Mock echo patterns —
whenever(repo.save(any())).thenAnswer { it.arguments[0] } followed by verify(repo).save(any()). You've tested that Mockito works, not that your code works. If you need to test persistence, hit a real database.
Auth/RBAC testing: consolidate, don't duplicate
If every controller has 5-10 tests like "returns 403 for VIEWER role", you have N_endpoints * M_roles nearly identical tests. Replace with:
- One parameterized security test that iterates all secured endpoints x roles and asserts the expected status code. This covers the RBAC matrix comprehensively.
- A few handwritten tests per controller for non-obvious auth behavior (custom permission logic, resource-level authorization, org isolation).
Core Cycle: Red-Green-Refactor
- Red — Write a failing test that defines expected behavior
- Green — Write the code to make the test pass
- Refactor — Improve code structure while keeping tests green
Never skip steps. Never write implementation before a failing test exists.
For planners: Define test cases in the plan with their level: [unit], [integration], [e2e] (per-test-case tagging is cheap — keep it). Use the selection heuristics above — don't default to [unit]. NEW repository methods, controller endpoints, event listeners, and schedulers should be [integration] by default. But when modifying existing, already-integration-tested persistence/endpoint code without changing its schema or contract, a targeted unit test of the changed logic is sufficient — rely on the existing integration suite.
For implementers: Implement tests one at a time through the cycle. Each test drives the next increment of design.
Implementation Strategy
Use Obvious Implementation by default — write the real implementation when the solution is clear. Fall back to Fake It (hardcoded values, then generalize) when the problem is genuinely uncertain or you keep hitting unexpected failures.
Test-First Process
Step 1: Analyze Requirements
Break the feature into discrete, testable behaviors. Each behavior becomes one test case. Order from simplest to most complex — degenerate/base cases first, then complexity.
Step 2: Write ONE Failing Test
Start with the simplest behavior. Write a single test that:
- Has a clear, descriptive name stating expected behavior
- Arranges minimal preconditions
- Acts on the unit under test
- Asserts one specific outcome
Run the test. Confirm it fails for the right reason (not a syntax error or import issue).
Do NOT write the next test yet. Each test drives the next design decision.
Step 3: Make It Pass
Write the code to make the test pass. Never write more production code than current tests require.
Step 4: Refactor
With the test green, improve: remove duplication, extract methods, improve naming, simplify logic. Run tests after every change.
Step 5: Repeat
Back to Step 2. Cycle: one test → pass → refactor → next test.
Test Quality Standards
Tests must be fast, isolated, deterministic, self-validating, and descriptively named. Follow Arrange-Act-Assert (AAA).
What to Test
Cover: happy paths, edge cases (empty, null, boundary), error cases, state transitions. Test through public APIs — not private internals.
Parameterization
When you see the same test structure repeated with different inputs, use parameterized tests instead of copy-pasting:
- JUnit 5:
@ParameterizedTest with @MethodSource or @CsvSource
- pytest:
@pytest.mark.parametrize
- Jest/Vitest:
test.each
- Go: table-driven tests
- Rust: macro-generated test cases or
rstest
Rule of thumb: if you're about to write a third test with the same structure but different data, parameterize.
Framework Detection
Check the project for existing test infrastructure before creating tests:
- Look for test configuration files (
jest.config, vitest.config, pytest.ini, build.gradle, pom.xml, go.mod, Cargo.toml, etc.)
- Look for existing test directories (
__tests__, test/, tests/, spec/, src/test/)
- Look for test runner scripts in
package.json, Makefile, etc.
- Look for E2E test setup (
playwright.config, cypress.config, e2e/, tests/e2e/)
- Match existing patterns — naming conventions, directory structure, assertion style
If .claude/devline.local.md exists, check for test_framework override.
Parallel Task Testing
In parallel pipelines: only test files in your task, mock other tasks' dependencies, ensure tests run independently. Cross-task integration tests belong in a dedicated task.
Running Tests
Always run the full test suite after implementation. Report: passed/failed/skipped counts, failure details, coverage changes if available.
Zero tolerance for failures. The feature branch starts green. Every test failure is signal — either your code is wrong, your change is incomplete, or the test needs updating to reflect intentionally changed behavior. Never dismiss failures as "pre-existing" or "unrelated."
Additional Resources
Reference Files
references/framework-patterns.md — Language-specific TDD patterns (JS/TS, Python, Go, Java, Rust, etc.)
references/advanced-tdd.md — Integration testing patterns, E2E strategies, mocking boundaries, anti-patterns
1---2name: kb-tdd-workflow3description: Domain logic for TDD methodology — injected into agents that implement code using test-driven development. Not invoked directly.4---56# TDD Workflow78Test-driven development produces better-designed, more maintainable code by writing tests before implementation. The critical skill is choosing the right test level — not defaulting to unit tests with mocks.910## Core Philosophy: Right Level, Real Dependencies11121. **Default to the highest useful level.** If a test can run against real infrastructure in under 5 seconds, it should. Don't mock what you can test for real.132. **Mock only at boundaries you don't own.** External APIs, email providers, payment gateways, third-party webhooks — mock these. Your own database, your own repositories, your own event bus, your own HTTP handlers — use the real thing.143. **Test behavior, not mechanics.** `verify(repo).save(any())` proves a method was called, not that the right thing was persisted. Assert on outcomes: query the database, check the HTTP response, verify the event payload. If you can only assert that a mock was invoked, you're testing wiring, not behavior.154. **Parameterize repetitive patterns.** If N endpoints x M roles all need the same auth check, write one parameterized test iterating all combinations. If 20 DTOs all validate `@NotBlank` on their name field, test the validation framework once, not 20 times.165. **Don't test the framework.** The framework is already tested by its maintainers.1718## Test Level Selection1920Choose the test level based on **what the code does**, not on convention or habit.2122### When to write integration tests (real DB, real HTTP, real infra)2324- **Persistence code** — any repository, DAO, or data access layer. Custom `@Query` methods, complex joins, constraint validation, cascades. These are the most bug-prone code in any project and the most dangerous to mock. Use Testcontainers, `@DataJpaTest`, in-memory databases, or test containers.25- **API endpoints** — controller/handler tests that verify request→response through the real routing, serialization, validation, and service layers. Use `@WebMvcTest`/`@SpringBootTest`, supertest, `httptest`, TestClient, etc.26- **Event-driven flows** — listeners, propagation engines, schedulers. Test that publishing an event produces the right side effects in the real database.27- **Migrations** — schema changes tested against a real database to catch column mismatches, constraint violations, data loss.28- **Multi-component interactions** — anywhere two or more of your own components collaborate and mocking one would hide real bugs (transaction boundaries, error propagation, ordering).2930### When to write unit tests (isolated, fast, no I/O)3132- **Pure business logic** — calculation engines, state machines, validation rules, parsers, formatters. These have clear inputs→outputs with no I/O.33- **Domain model behavior** — methods on entities/value objects that compute something.34- **Algorithm-heavy code** — sorting, filtering, transformation, scoring.35- **Error path logic** — retry strategies, circuit breakers, fallback chains (logic only, not the actual I/O).3637### When to write E2E tests3839- **Complete user journeys** — multi-step flows from entry point to observable outcome (signup→verify→first use, create→classify→propagate).40- **Cross-boundary flows** — requests that traverse multiple services or modules.41- **Critical business paths** — the paths where a bug means revenue loss, compliance violation, or data corruption.4243E2E tests are defined at the **feature level** by the planner (see Feature-Goal Tests in the plan), not per-task. Include a dedicated final-wave E2E task ONLY for features with a genuine multi-step cross-boundary journey; skip it for single-component changes, pure-logic changes, and bugfixes (per-task integration tests already cover that surface). When present, it runs after all implementation is merged.4445## Test Depth4647A per-feature dial (from `.claude/devline.local.md` `test_depth`, or inferred during brainstorm). Two levels:4849- **deep** — exhaustive: a unit test per method plus edge cases and all configs, plus integration and E2E. This is the current default thoroughness.50- **focused** — big behavior tests over whole classes/workflows plus targeted tests for genuinely hard logic; integration/E2E for real journeys; SKIP exhaustive per-method unit tests for trivial code (getters, passthroughs, obvious branches).5152### Acceptance criteria as tests5354The brainstorm defines behavioral **acceptance criteria**. Each criterion becomes ONE behavior/workflow-level test, named to read as the criterion — the test name IS the spec sentence. There are no durable spec docs; the committed tests ARE the living spec.5556Under `focused`, these acceptance tests are the **primary suite**: do NOT write a unit test per method for trivial code — only for genuinely hard or edge logic. Under `deep`, the acceptance tests sit on top of the exhaustive per-method units.5758`focused` changes nothing about level selection: per-test `[unit]`/`[integration]`/`[e2e]` tagging stays, NEW I/O (persistence, endpoints, events) is still `[integration]` by default, the E2E task stays **SHOULD** (only for genuine cross-boundary journeys), and the integration size-gate — a targeted unit test suffices when modifying existing integration-tested code without changing its schema/contract — still applies. `focused` only drops the redundant per-method units for trivial code; it never downgrades a real journey or a new I/O surface.5960## What NOT to Test6162These produce noise without catching bugs. Delete them if they exist; don't write new ones.6364- **Framework behavior** — `@NotBlank` rejects blank strings, `@Valid` triggers validation, Spring Security enforces `@PreAuthorize`. The framework is tested by its own test suite. Only test YOUR validation logic (custom validators, conditional rules, business constraints).65- **Language features** — data class defaults, null safety, enum values, constructor parameters. If Kotlin/Java/TS guarantees it, don't test it.66- **Delegation methods** — one-liner passthrough methods that just call another method. Testing `helper.doThing()` calls `service.doThing()` verifies nothing useful.67- **Trivial getters/setters** — unless they contain logic.68- **Mock echo patterns** — `whenever(repo.save(any())).thenAnswer { it.arguments[0] }` followed by `verify(repo).save(any())`. You've tested that Mockito works, not that your code works. If you need to test persistence, hit a real database.6970### Auth/RBAC testing: consolidate, don't duplicate7172If every controller has 5-10 tests like "returns 403 for VIEWER role", you have N_endpoints * M_roles nearly identical tests. Replace with:73741. **One parameterized security test** that iterates all secured endpoints x roles and asserts the expected status code. This covers the RBAC matrix comprehensively.752. **A few handwritten tests** per controller for non-obvious auth behavior (custom permission logic, resource-level authorization, org isolation).7677## Core Cycle: Red-Green-Refactor78791. **Red** — Write a failing test that defines expected behavior802. **Green** — Write the code to make the test pass813. **Refactor** — Improve code structure while keeping tests green8283Never skip steps. Never write implementation before a failing test exists.8485**For planners:** Define test cases in the plan with their level: `[unit]`, `[integration]`, `[e2e]` (per-test-case tagging is cheap — keep it). Use the selection heuristics above — don't default to `[unit]`. NEW repository methods, controller endpoints, event listeners, and schedulers should be `[integration]` by default. But when modifying existing, already-integration-tested persistence/endpoint code **without changing its schema or contract**, a targeted unit test of the changed logic is sufficient — rely on the existing integration suite.8687**For implementers:** Implement tests one at a time through the cycle. Each test drives the next increment of design.8889## Implementation Strategy9091Use **Obvious Implementation** by default — write the real implementation when the solution is clear. Fall back to **Fake It** (hardcoded values, then generalize) when the problem is genuinely uncertain or you keep hitting unexpected failures.9293## Test-First Process9495### Step 1: Analyze Requirements9697Break the feature into discrete, testable behaviors. Each behavior becomes one test case. Order from simplest to most complex — degenerate/base cases first, then complexity.9899### Step 2: Write ONE Failing Test100101Start with the simplest behavior. Write a single test that:102- Has a clear, descriptive name stating expected behavior103- Arranges minimal preconditions104- Acts on the unit under test105- Asserts one specific outcome106107Run the test. Confirm it fails for the right reason (not a syntax error or import issue).108109**Do NOT write the next test yet.** Each test drives the next design decision.110111### Step 3: Make It Pass112113Write the code to make the test pass. Never write more production code than current tests require.114115### Step 4: Refactor116117With the test green, improve: remove duplication, extract methods, improve naming, simplify logic. Run tests after every change.118119### Step 5: Repeat120121Back to Step 2. Cycle: one test → pass → refactor → next test.122123## Test Quality Standards124125Tests must be fast, isolated, deterministic, self-validating, and descriptively named. Follow Arrange-Act-Assert (AAA).126127### What to Test128129Cover: happy paths, edge cases (empty, null, boundary), error cases, state transitions. Test through public APIs — not private internals.130131### Parameterization132133When you see the same test structure repeated with different inputs, use parameterized tests instead of copy-pasting:134135- **JUnit 5:** `@ParameterizedTest` with `@MethodSource` or `@CsvSource`136- **pytest:** `@pytest.mark.parametrize`137- **Jest/Vitest:** `test.each`138- **Go:** table-driven tests139- **Rust:** macro-generated test cases or `rstest`140141Rule of thumb: if you're about to write a third test with the same structure but different data, parameterize.142143## Framework Detection144145Check the project for existing test infrastructure before creating tests:1461471. Look for test configuration files (`jest.config`, `vitest.config`, `pytest.ini`, `build.gradle`, `pom.xml`, `go.mod`, `Cargo.toml`, etc.)1482. Look for existing test directories (`__tests__`, `test/`, `tests/`, `spec/`, `src/test/`)1493. Look for test runner scripts in `package.json`, `Makefile`, etc.1504. Look for E2E test setup (`playwright.config`, `cypress.config`, `e2e/`, `tests/e2e/`)1515. Match existing patterns — naming conventions, directory structure, assertion style152153If `.claude/devline.local.md` exists, check for `test_framework` override.154155## Parallel Task Testing156157In parallel pipelines: only test files in your task, mock other tasks' dependencies, ensure tests run independently. Cross-task integration tests belong in a dedicated task.158159## Running Tests160161Always run the full test suite after implementation. Report: passed/failed/skipped counts, failure details, coverage changes if available.162163**Zero tolerance for failures.** The feature branch starts green. Every test failure is signal — either your code is wrong, your change is incomplete, or the test needs updating to reflect intentionally changed behavior. Never dismiss failures as "pre-existing" or "unrelated."164165## Additional Resources166167### Reference Files168169- **`references/framework-patterns.md`** — Language-specific TDD patterns (JS/TS, Python, Go, Java, Rust, etc.)170- **`references/advanced-tdd.md`** — Integration testing patterns, E2E strategies, mocking boundaries, anti-patterns