Philosophy
Core principle: Tests should verify behavior through public interfaces, not implementation details. Code can change completely; tests shouldn't.
Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't.
Good tests are integration-style: they exercise real code paths through public APIs. They describe what the systemdoes, nothow. A good test reads like a specification—"user can checkout with a valid cart" tells you exactly what capability exists. These tests survive refactoring because they don't care about internal structure.
Good tests are integration-style: they exercise real code paths through public APIs. They describe what the system does, not how it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure.
Bad tests are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying the database directly instead of using the interface). Warning sign: your test fails when you refactor but behavior hasn't changed. If you rename an internal function and the test fails, those tests are testing implementation, not behavior.
Bad tests are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
See tests.md for examples,mocking.md for mocking guidelines.
See tests.md for examples and mocking.md for mocking guidelines.
Anti-Pattern: Horizontal Slices
Don't write all tests first, then all implementation. This is "horizontal slicing"—treating RED as "write all tests" and GREEN as "write all code."
DO NOT write all tests first, then all implementation. This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code."
This producesbad tests:
This produces crap tests:
- Tests written in bulk testimaginedbehavior, notactualbehavior
Tests written in bulk test imagined behavior, not actual behavior
- You end up testing theshape(data structures, function signatures) rather than user-facing behavior
You end up testing the shape of things (data structures, function signatures) rather than user-facing behavior
- Tests become insensitive to real changes—they pass when behavior breaks, they fail when behavior works
Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine
- You go beyond the headlights' range, committing to test structure before understanding the implementation
You outrun your headlights, committing to test structure before understanding the implementation
Correct approach: Vertical slicing through tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. Because you just wrote the code, you know exactly what behavior matters and how to verify it.
Vertical slices via tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. Because you just wrote the code, you know exactly what behavior matters and how to verify it.
错误 (WRONG) (水平 horizontal):
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
正确 (RIGHT) (垂直 vertical):
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
...
Workflow
1. Planning
When exploring the codebase, use the project's domain glossary so test names and interface vocabulary match the project's language, and respect the ADRs of the area you're modifying.
When exploring the codebase, use the project's domain glossary so that test names and interface vocabulary match the project's language, and respect ADRs in the area you're touching.
Before writing any code:
Before writing any code:
- Confirm with the user which interface changes are needed
Confirm with user what interface changes are needed
- Confirm with the user which behaviors to test (prioritized)
Confirm with user which behaviors to test (prioritize)
- Identifydeep modulesopportunities (small interface, deep implementation)
Identify opportunities for deep modules (small interface, deep implementation)
- Fortestabilitydesign interfaces
Design interfaces for testability
- List the behaviors to test (not implementation steps)
List the behaviors to test (not implementation steps)
- Get user approval of the plan
Get user approval on the plan
Q: "What should the public interface look like? Which behaviors are most important to test?"
Ask: "What should the public interface look like? Which behaviors are most important to test?"
You can't test everything. Confirm with the user which behaviors are most important. Focus testing effort on critical paths and complex logic, rather than every possible edge case.
You can't test everything. Confirm with the user exactly which behaviors matter most. Focus testing effort on critical paths and complex logic, not every possible edge case.
2. Tracer Bullet
Write one test to confirm one thing about the system:
Write ONE test that confirms ONE thing about the system:
RED: 为第一个行为写测试 → 测试失败
GREEN: 写最少代码使其通过 → 测试通过
RED: Write test for first behavior → test fails
GREEN: Write minimal code to pass → test passes
This is your tracer bullet—proving the path works end to end.
This is your tracer bullet - proves the path works end-to-end.
3. Incremental Loop
For each remaining behavior:
For each remaining behavior:
RED: 写下一个测试 → 失败
GREEN: 最少代码使其通过 → 通过
RED: Write next test → fails
GREEN: Minimal code to pass → passes
Rules:
Rules:
- One test at a time
One test at a time
- Write only the minimum code needed to make the current test pass
Only enough code to pass current test
- Don't anticipate future tests
Don't anticipate future tests
- Keep tests focused on observable behavior
Keep tests focused on observable behavior
4. Refactor
After all tests pass, look forrefactoring candidates:
After all tests pass, look for refactor candidates:
- Extract duplication
- Deepen modules (move complexity behind simple interfaces)
Deepen modules (move complexity behind simple interfaces)
- Apply SOLID principles where natural
Apply SOLID principles where natural
- Consider what problems the new code reveals in the existing code
Consider what new code reveals about existing code
- Run tests after each refactoring step
Run tests after each refactor step
Never refactor while RED. Get to GREEN first.
Never refactor while RED. Get to GREEN first.
Checklist Per Cycle
[ ] 测试描述行为,不是实现
[ ] Test describes behavior, not implementation
[ ] 测试仅使用公共接口
[ ] Test uses public interface only
[ ] 测试会在内部重构中存活
[ ] Test would survive internal refactor
[ ] 代码对此测试是最少的
[ ] Code is minimal for this test
[ ] 未添加投机性功能
[ ] No speculative features added