Philosophy
Core principle: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't.
Good testsare integration-style: they exercise real code paths through the public API. They describe what the system _does_, not _how_ it does it. A good test reads like a specification—"the user can check out with a valid cart" tells you exactly what capability exists. These tests survive refactoring because they don't care about internal structure.
Bad testsare coupled to the implementation. They mock internal collaborators, test private methods, or verify through external means (such as querying the database directly instead of using the interface). Warning sign: your tests fail when you refactor, but the behavior hasn't changed. If your tests fail when you rename an internal function, those tests are testing the implementation, not the behavior.
See tests.md for examples,mocking.md for mocking guidance.
Anti-pattern: horizontal slicing
Don't write all the tests first, then write all the implementation. This is "horizontal slicing"—treating RED as "write all the tests" and GREEN as "write all the code."
This producesbad tests:
- Tests written in bulk test _imagined_ behavior, not _actual_ behavior
- You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior
- Tests are insensitive to real changes—they pass when behavior breaks and fail when behavior is fine
- You run past your headlights, committing to the test structure before you understand the implementation
The right 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.
错误(水平):
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
正确(垂直):
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
...
Workflow
1. Planning
While exploring the codebase, use the project's domain glossary so that test names and interface vocabulary match the project's language, and respect the ADRs in the area you are touching.
Before writing any code:
- Confirm with the user which interface changes are needed
- Confirm with the user which behaviors to test (priority)
- Identify deep modules opportunities (small interfaces, deep implementations)
- For testability design interfaces
- List the behaviors to test (not implementation steps)
- Get the user's approval of the plan
Ask: "What should the public interface look like? Which behaviors are most important to test?"
You can't test everything. Confirm with the user exactly which behaviors are most important. Focus test 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:
RED: 为第一个行为编写测试 → 测试失败
GREEN: 编写最小代码以通过 → 测试通过
This is your tracer bullet—proving the path works end to end.
3. Incremental loop
For each remaining behavior:
RED: 编写下一个测试 → 失败
GREEN: 最小代码以通过 → 通过
Rules:
- One test at a time
- Only enough code to pass the current test
- Do not predict future tests
- Keep tests focused on observable behavior
4. Refactor
After all tests pass, look for refactoring candidates:
- Extract duplication
- Deepen modules (move complexity behind simple interfaces)
- Apply SOLID principles where natural
- Consider what the new code reveals about the existing code
- Run tests after each refactoring step
Never refactor while RED. Get to GREEN first.
Checklist for each cycle
[ ] 测试描述行为,而不是实现
[ ] 测试仅使用公共接口
[ ] 测试会在内部重构中存活
[ ] 代码对此测试是最小的
[ ] 没有添加推测性功能