Testing — Strategy & Philosophy
Testing is not verification after the fact. It is feedback on design. A test that is hard to write reveals coupling in production code. A test that is hard to name reveals unclear thinking about behavior. The test suite is the living specification of the system — what the code is actually supposed to do. That's true whether a human or an AI agent wrote the code.
Core Beliefs
| Belief |
What it means in practice |
Anti-pattern it prevents |
| Hard to test = design problem |
Refactor the design, not the test |
Mocking everything to force testability |
| Tests are specification, not verification |
Name tests as sentences describing behavior |
Tests named after implementation details |
| Mocks are a smell, not a strategy |
Prefer real collaborators or fakes |
Mock-heavy suites that survive bugs |
| Pure functions are the testability ideal |
Push I/O to the edges; keep business logic pure |
Business logic tangled with side effects |
| Green must mean releasable |
A failing build is a stop-the-line signal, not noise |
Retrying flaky tests until the suite happens to pass |
| Test behavior, not design principles |
SRP and other architectural rules guide how code is structured; assert what the code does, not whether it obeys a design rule |
Tests that count methods, check class responsibilities, or assert SOLID compliance instead of observable outcomes |
The Test Portfolio
The test pyramid describes a useful default distribution, not a law. Unit tests form the base because they are fast and precise. Higher-level tests are valuable because they exercise wiring, collaboration, and user journeys. The portfolio rule is: choose the cheapest layer that gives the scenario the confidence it needs.
| Layer |
Count |
Speed |
Purpose |
Fragility |
| Unit |
Many |
ms |
Pure behavior specification |
Low if behavior-focused |
| Component |
Some |
ms–seconds |
One deployable service through its public interface |
Low–medium |
| Integration |
Some |
seconds |
Real collaborators in one process/system |
Medium |
| Contract |
Some |
ms–seconds |
Consumer/provider expectations at a service boundary |
Low if contract-focused |
| E2E / UI |
Few |
seconds–minutes |
First-of-its-kind user journey |
High — minimize |
| Production |
Ongoing |
near-real-time |
Synthetic monitoring and domain observability |
Operational — not a substitute |
When a high-level test finds a bug, reproduce it at the lowest useful level before fixing it. The focused regression test is the permanent guardrail; the high-level test is the discovery mechanism.
Decision Guide
| Question |
Answer → Action |
| Is this pure behavior or a wiring concern? |
Pure behavior → unit test; wiring → component/integration test |
| Does the scenario cross a service boundary? |
Yes → contract test for consumer/provider expectations; keep E2E for one journey |
| Is this a critical user journey? |
Write one E2E test, then push variations to lower layers |
| Do I need mocks, or can I use real collaborators? |
Real collaborators preferred; mocks only when the outgoing interaction itself is the behavior |
| Is the test hard to write? |
Treat that as design feedback; refactor before adding more doubles |
| Does the test depend on timing or non-deterministic I/O? |
Fix the design; fast tests must be deterministic |
| Have I seen this test fail when it should? |
No → inject an obvious bug, confirm the failure, revert it |
| Would production behavior surprise us? |
Add synthetic monitoring, domain observability, or exploratory investigation |
| Who's driving — human or AI agent? |
Human → tcrdd micro-steps; Agent → test-first batches with separate trust signals |
Testing With AI Coding Agents
Strict red/green/refactor exists to manage human short-term memory. Agents have the opposite profile — large working memory, but prone to hallucination and to gaming the test rather than satisfying it. The cadence and trust model both need to adapt.
| Old assumption |
Agent-era adjustment |
| Force micro-step TDD for tight feedback |
Let the agent write tests first, then implement in larger batches |
| The author can test their own code |
Separate test-writer from coder roles to avoid tests that codify the bug |
| Code review is the primary catch-all |
Combine coverage, mutation testing, complexity metrics, and targeted human review |
| Acceptance tests are ordinary code artifacts |
Treat them as immutable guardrails for the coding agent |
| Smaller diff is always better |
Prefer coherent test grouping over scattering tests for a smaller diff |
| Production checks are optional |
Monitor prompt/output quality, drift, latency, and failure modes continuously |
For the full pipeline and trust model, see Testing With AI Coding Agents. For LLM applications, also read Martin Fowler's Testing Canon.
Testing Anti-Patterns
| Anti-pattern |
Problem |
Fix |
| Over-mocking |
Mocks hide integration bugs |
Prefer real collaborators or fakes |
| Testing implementation |
Tests break on refactor |
Test observable behavior, not method call graphs |
| Brittle/flaky tests |
Results become noise |
Isolate state; control time, randomness, and async completion |
| Slow unit tests |
Design is too coupled |
Refactor the design, not the test |
| Top-heavy E2E suite |
Diminishing returns; every variation pays full cost |
Keep one E2E path and push variations to lower layers |
| Service boundary tested only by E2E |
Expensive and fragile |
Add consumer-driven contract tests |
| Logic inside tests |
The test can carry the same bug it should catch |
Use hardcoded expected values and no control flow |
| Coverage treated as quality |
High coverage can still mean weak assertions |
Use coverage to find gaps; use mutation/contract tests to test strength |
| No production feedback loop |
Unknown failure modes remain invisible |
Add observability, synthetic monitoring, and exploratory testing |
| Skip legacy testing |
Risk grows while refactoring |
Build a characterization net before changing behavior |
| AI agent tests its own code |
Tests codify the bug |
Separate test-writer and coder roles |
| Agent weakens acceptance tests to pass |
The guardrail is defeated |
Make acceptance tests immutable for the coding agent |
| Testing architecture rules as behavior |
Structural rules are not user-visible behavior |
Enforce architecture with static analysis; test observable outcomes |
Integrated Example
Before — hard to test (the design is the problem):
function sendOverdueReminders() {
const users = db.query("SELECT * FROM users WHERE balance < 0");
for (const user of users) {
if (Date.now() - user.lastReminded > WEEK) {
emailClient.send(user.email, "You owe us money");
}
}
}
This function forces a test to control a database, a clock, and an email client. The business rule — who should be reminded — is buried inside the side effects.
Diagnosis: push I/O to the edges and keep the rule pure.
After — pure core, I/O at the edge:
function usersToRemind(users, now) {
return users.filter(
(user) => user.balance < 0 && now - user.lastReminded > WEEK,
);
}
function sendOverdueReminders() {
for (const user of usersToRemind(db.allUsers(), Date.now())) {
emailClient.send(user.email, "You owe us money");
}
}
usersToRemind([...], fixedNow) is a one-line unit test with no doubles. The remaining shell is a thin integration concern. Hard-to-test became easy-to-test by changing the design, not by adding mocks.
Read On Demand
| Read when |
File |
| Understanding the four cross-cutting testing principles |
Testing Principles |
| Auditing test quality against the 12 desiderata properties |
TestDesiderata — Quality Audit |
| Reviewing tests for BDD compliance and structure |
BDD Test Review |
| Directing an AI coding agent: cadence, pipeline, trust model |
Testing With AI Coding Agents |
| Choosing what to mock and what to assert |
Mocks & Fragility |
| Auditing trustworthiness, maintainability, and readability |
Trustworthy & Maintainable Tests |
| Distributing scenarios across test layers; test recipes; legacy triage |
Test Strategy & Legacy Code |
| Testing async code, timers, events, or faking modules |
Async, Time & Module Faking |
| Applying Fowler-style portfolio, doubles, production feedback, and LLM practices |
Martin Fowler's Testing Canon |
| Identifying and writing smoke tests for CI gates |
Dedicated smoke-tests skill |
| Driving behavior from Given/When/Then scenarios; crap + mutation quality gates |
Acceptance-Testing Workflow |
Specialist Skills
| Situation |
Specialist skill |
Why |
| Need interactive, human-paced TDD practice |
tcrdd |
Red/green/refactor cadence with immediate feedback |
| Identifying or writing smoke tests |
smoke-tests |
Dedicated triggers, template, and CI integration |
Benchmark
Scenario: .benchmarks/scenarios/testing-001-agent-test-strategy.md · Run: 2026-08-31 · Log: .benchmarks/runs/2026-08-31/testing-001-agent-test-strategy.json
| Model |
Without |
With |
Delta |
| claude-opus-4-8 |
67% |
100% |
+33% |
| claude-sonnet-4-6 |
100% |
100% |
+0% |
| claude-haiku-4-5 |
67% |
100% |
+33% |
PASS (run 2026-08-31). Opus and haiku +33; sonnet at ceiling. Gate per .agents/skills/skill-optimizer/rules/release-gates.md.
1---2name: testing3description: Testing strategy and philosophy — design feedback, test quality audit, BDD review, architecture decisions, and test strategy for AI coding agents. TRIGGER when: strategy (testing approach, philosophy, methodology, how should I test, what tests to write), portfolio (unit, integration, component, contract, E2E, testing layers, how much to test), quality (brittle/flaky/slow/hard-to-maintain tests, testing anti-patterns, can I trust this test), mocking (when to use mocks, test doubles, stubs, fakes), review (review my test suite, BDD review), context (testing legacy codebase, adding tests to existing code), production (QA in production, synthetic monitoring, observability, exploratory testing), LLM applications (testing LLM systems, prompts, retrieval, evaluation suites), AI agents (testing strategy for AI-generated code, should the AI write its own tests, multi-agent testing). DO NOT USE for interactive TDD cadence with red/green/refactor loops — use `tcrdd` instead. DO NOT USE for smoke tests specifically — use4---56# Testing — Strategy & Philosophy78Testing is not verification after the fact. It is feedback on design. A test that is hard to write reveals coupling in production code. A test that is hard to name reveals unclear thinking about behavior. The test suite is the living specification of the system — what the code is actually supposed to do. That's true whether a human or an AI agent wrote the code.910---1112## Core Beliefs1314| Belief | What it means in practice | Anti-pattern it prevents |15| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |16| Hard to test = design problem | Refactor the design, not the test | Mocking everything to force testability |17| Tests are specification, not verification | Name tests as sentences describing behavior | Tests named after implementation details |18| Mocks are a smell, not a strategy | Prefer real collaborators or fakes | Mock-heavy suites that survive bugs |19| Pure functions are the testability ideal | Push I/O to the edges; keep business logic pure | Business logic tangled with side effects |20| Green must mean releasable | A failing build is a stop-the-line signal, not noise | Retrying flaky tests until the suite happens to pass |21| Test behavior, not design principles | SRP and other architectural rules guide _how_ code is structured; assert _what_ the code does, not whether it obeys a design rule | Tests that count methods, check class responsibilities, or assert SOLID compliance instead of observable outcomes |2223---2425## The Test Portfolio2627The test pyramid describes a useful default distribution, not a law. Unit tests form the base because they are fast and precise. Higher-level tests are valuable because they exercise wiring, collaboration, and user journeys. The portfolio rule is: choose the cheapest layer that gives the scenario the confidence it needs.2829| Layer | Count | Speed | Purpose | Fragility |30| --------------- | ------- | --------------- | ---------------------------------------------------- | ------------------------------ |31| **Unit** | Many | ms | Pure behavior specification | Low if behavior-focused |32| **Component** | Some | ms–seconds | One deployable service through its public interface | Low–medium |33| **Integration** | Some | seconds | Real collaborators in one process/system | Medium |34| **Contract** | Some | ms–seconds | Consumer/provider expectations at a service boundary | Low if contract-focused |35| **E2E / UI** | Few | seconds–minutes | First-of-its-kind user journey | High — minimize |36| **Production** | Ongoing | near-real-time | Synthetic monitoring and domain observability | Operational — not a substitute |3738When a high-level test finds a bug, reproduce it at the lowest useful level before fixing it. The focused regression test is the permanent guardrail; the high-level test is the discovery mechanism.3940---4142## Decision Guide4344| Question | Answer → Action |45| -------------------------------------------------------- | --------------------------------------------------------------------------------------------- |46| Is this pure behavior or a wiring concern? | Pure behavior → unit test; wiring → component/integration test |47| Does the scenario cross a service boundary? | Yes → contract test for consumer/provider expectations; keep E2E for one journey |48| Is this a critical user journey? | Write one E2E test, then push variations to lower layers |49| Do I need mocks, or can I use real collaborators? | Real collaborators preferred; mocks only when the outgoing interaction itself is the behavior |50| Is the test hard to write? | Treat that as design feedback; refactor before adding more doubles |51| Does the test depend on timing or non-deterministic I/O? | Fix the design; fast tests must be deterministic |52| Have I seen this test fail when it should? | No → inject an obvious bug, confirm the failure, revert it |53| Would production behavior surprise us? | Add synthetic monitoring, domain observability, or exploratory investigation |54| Who's driving — human or AI agent? | Human → `tcrdd` micro-steps; Agent → test-first batches with separate trust signals |5556---5758## Testing With AI Coding Agents5960Strict red/green/refactor exists to manage _human_ short-term memory. Agents have the opposite profile — large working memory, but prone to hallucination and to gaming the test rather than satisfying it. The cadence and trust model both need to adapt.6162| Old assumption | Agent-era adjustment |63| -------------------------------------------- | --------------------------------------------------------------------------------- |64| Force micro-step TDD for tight feedback | Let the agent write tests first, then implement in larger batches |65| The author can test their own code | Separate test-writer from coder roles to avoid tests that codify the bug |66| Code review is the primary catch-all | Combine coverage, mutation testing, complexity metrics, and targeted human review |67| Acceptance tests are ordinary code artifacts | Treat them as immutable guardrails for the coding agent |68| Smaller diff is always better | Prefer coherent test grouping over scattering tests for a smaller diff |69| Production checks are optional | Monitor prompt/output quality, drift, latency, and failure modes continuously |7071For the full pipeline and trust model, see [Testing With AI Coding Agents](references/ai-agent-testing.md). For LLM applications, also read [Martin Fowler's Testing Canon](references/martin-fowler-testing.md).7273---7475## Testing Anti-Patterns7677| Anti-pattern | Problem | Fix |78| -------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------- |79| Over-mocking | Mocks hide integration bugs | Prefer real collaborators or fakes |80| Testing implementation | Tests break on refactor | Test observable behavior, not method call graphs |81| Brittle/flaky tests | Results become noise | Isolate state; control time, randomness, and async completion |82| Slow unit tests | Design is too coupled | Refactor the design, not the test |83| Top-heavy E2E suite | Diminishing returns; every variation pays full cost | Keep one E2E path and push variations to lower layers |84| Service boundary tested only by E2E | Expensive and fragile | Add consumer-driven contract tests |85| Logic inside tests | The test can carry the same bug it should catch | Use hardcoded expected values and no control flow |86| Coverage treated as quality | High coverage can still mean weak assertions | Use coverage to find gaps; use mutation/contract tests to test strength |87| No production feedback loop | Unknown failure modes remain invisible | Add observability, synthetic monitoring, and exploratory testing |88| Skip legacy testing | Risk grows while refactoring | Build a characterization net before changing behavior |89| AI agent tests its own code | Tests codify the bug | Separate test-writer and coder roles |90| Agent weakens acceptance tests to pass | The guardrail is defeated | Make acceptance tests immutable for the coding agent |91| Testing architecture rules as behavior | Structural rules are not user-visible behavior | Enforce architecture with static analysis; test observable outcomes |9293---9495## Integrated Example9697**Before — hard to test (the design is the problem):**9899```js100function sendOverdueReminders() {101 const users = db.query("SELECT * FROM users WHERE balance < 0");102 for (const user of users) {103 if (Date.now() - user.lastReminded > WEEK) {104 emailClient.send(user.email, "You owe us money");105 }106 }107}108```109110This function forces a test to control a database, a clock, and an email client. The business rule — _who should be reminded_ — is buried inside the side effects.111112**Diagnosis:** push I/O to the edges and keep the rule pure.113114**After — pure core, I/O at the edge:**115116```js117function usersToRemind(users, now) {118 return users.filter(119 (user) => user.balance < 0 && now - user.lastReminded > WEEK,120 );121}122123function sendOverdueReminders() {124 for (const user of usersToRemind(db.allUsers(), Date.now())) {125 emailClient.send(user.email, "You owe us money");126 }127}128```129130`usersToRemind([...], fixedNow)` is a one-line unit test with no doubles. The remaining shell is a thin integration concern. Hard-to-test became easy-to-test by changing the design, not by adding mocks.131132---133134## Read On Demand135136| Read when | File |137| -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |138| Understanding the four cross-cutting testing principles | [Testing Principles](references/principles.md) |139| Auditing test quality against the 12 desiderata properties | [TestDesiderata — Quality Audit](references/testdesiderata.md) |140| Reviewing tests for BDD compliance and structure | [BDD Test Review](references/bdd-review.md) |141| Directing an AI coding agent: cadence, pipeline, trust model | [Testing With AI Coding Agents](references/ai-agent-testing.md) |142| Choosing what to mock and what to assert | [Mocks & Fragility](references/mocks-and-fragility.md) |143| Auditing trustworthiness, maintainability, and readability | [Trustworthy & Maintainable Tests](references/trustworthy-tests.md) |144| Distributing scenarios across test layers; test recipes; legacy triage | [Test Strategy & Legacy Code](references/test-strategy.md) |145| Testing async code, timers, events, or faking modules | [Async, Time & Module Faking](references/async-and-time.md) |146| Applying Fowler-style portfolio, doubles, production feedback, and LLM practices | [Martin Fowler's Testing Canon](references/martin-fowler-testing.md) |147| Identifying and writing smoke tests for CI gates | Dedicated `smoke-tests` skill |148| Driving behavior from Given/When/Then scenarios; crap + mutation quality gates | [Acceptance-Testing Workflow](references/acceptance-testing.md) |149150---151152## Specialist Skills153154| Situation | Specialist skill | Why |155| ------------------------------------------ | ---------------- | -------------------------------------------------- |156| Need interactive, human-paced TDD practice | `tcrdd` | Red/green/refactor cadence with immediate feedback |157| Identifying or writing smoke tests | `smoke-tests` | Dedicated triggers, template, and CI integration |158159---160161## Benchmark162163Scenario: `.benchmarks/scenarios/testing-001-agent-test-strategy.md` · Run: 2026-08-31 · Log: `.benchmarks/runs/2026-08-31/testing-001-agent-test-strategy.json`164165| Model | Without | With | Delta |166| ----------------- | ------- | ---- | ----- |167| claude-opus-4-8 | 67% | 100% | +33% |168| claude-sonnet-4-6 | 100% | 100% | +0% |169| claude-haiku-4-5 | 67% | 100% | +33% |170171> **PASS (run 2026-08-31)**. Opus and haiku +33; sonnet at ceiling. Gate per `.agents/skills/skill-optimizer/rules/release-gates.md`.