test-driven-development
Test-first implementation discipline: write the failing test, watch it fail, write minimal code to pass, refactor. Language- and framework-agnostic. Use when implementing any feature or bugfix, before writing implementation code.
- License 许可
- MIT
- 最近更新
- 2026-11-05
- Stars
- ★ 2
- Forks
- 0
01 前言 Front Matter
- name
- test-driven-development
- license
- MIT
- metadata
- { "author": "shahboura (methodology adapted from obra/superpowers, MIT)", "version": "1.0.0", "audience": "developers", "workflow": "development" }
- description
- Test-first implementation discipline: write the failing test, watch it fail, write minimal code to pass, refactor. Language- and framework-agnostic. Use when implementing any feature or bugfix, before writing implementation code.
- compatibility
- opencode
02 资源文件 1
- 参考文档references/writing-good-tests.md7.5 KB
03 SKILL.md 正文
Test-Driven Development (TDD)
What I do
- Enforce write-test-first: no production code without a failing test
- Drive the RED → GREEN → REFACTOR cycle with verification at each step
- Keep tests honest (see
references/writing-good-tests.md)
When to use me
Always: new features, bug fixes, refactoring, behavior changes.
Exceptions (ask the user): generated code, configuration files, throwaway prototypes. Visual/UI work is not exempt by default — ask the user, and a Playwright or screenshot regression check may substitute for a unit test.
Thinking "skip TDD just this once"? Stop — that is rationalization.
The Iron Law
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
Wrote code before the test — code you wrote in this session and have not committed? Delete it and start over: do not keep it as "reference", do not adapt it, do not look at it. Implement fresh from tests. Never delete pre-existing, committed, or user-authored code; if the situation is unclear, ask the user before removing anything.
The cycle
- RED — write one minimal test showing what should happen.
- Verify RED — run it and watch it fail for the expected reason (feature missing, not a typo). A test that passes immediately is testing existing behavior; fix the test.
- GREEN — write the simplest code that passes. No extra features, no refactoring beyond the test.
- Verify GREEN — run it; the new test passes and the project's suite stays green. Run the project's own test command, not just your file, and report any failure by name.
- REFACTOR — remove duplication, improve names, extract helpers; stay green; add no behavior.
- Repeat — next failing test for the next feature.
Commits are a separate concern: follow the repository's review/commit gate rather than committing once per cycle.
Good RED — clear name, real behavior, one thing
test('retries failed operations 3 times', async () => {
let attempts = 0;
const operation = () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retryOperation(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});
Bad RED — vague name, tests the mock instead of the code
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
Good GREEN — just enough to pass
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
for (let i = 0; i < 3; i++) {
try {
return await fn();
} catch (e) {
if (i === 2) throw e;
}
}
throw new Error('unreachable');
}
Bad GREEN — over-engineered beyond the test
async function retryOperation<T>(
fn: () => Promise<T>,
options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; onRetry?: (n: number) => void }
): Promise<T> { /* YAGNI */ }
Good tests
| Quality | Good | Bad |
|---|---|---|
| Minimal | One thing; "and" in the name → split it | test('validates email and domain and whitespace') |
| Clear | Name describes the behavior | test('test1') |
| Shows intent | Demonstrates the desired API | Obscures what the code should do |
When writing or changing any test, read references/writing-good-tests.md:
name the break the test catches, derive expectations by hand, assert real
behavior (never the mock), and keep test-only code in test utilities.
Red flags — stop and start over
- Code written before the test
- Test passes immediately
- Cannot explain why the test failed
- "I'll add tests later" / "I already manually tested it"
- "Keep as reference" or "adapt existing code"
- "TDD is dogmatic, I'm being pragmatic"
Common rationalizations
| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks; the test takes 30 seconds. |
| "I'll test after" | Tests written after pass immediately — which proves nothing. |
| "Tests after achieve the same goal" | Tests-after ask "what does this do?"; tests-first ask "what should this do?" |
| "Already manually tested" | Manual testing is ad-hoc: no record, no re-run, easy to miss cases. |
| "Deleting hours of work is wasteful" | Sunk cost — keeping code you cannot trust is the real waste. |
| "TDD slows me down" | TDD catches bugs before commit and lets you refactor without fear. |
When stuck
| Problem | Solution |
|---|---|
| Do not know how to test | Write the wished-for API; write the assertion first; ask the user. |
| Test too complicated | The design is too complicated — simplify the interface. |
| Must mock everything | The code is too coupled — use dependency injection. |
Debugging integration
Found a bug? Write a failing test that reproduces it first, then follow the
cycle — the test proves the fix and prevents regression. Security findings need
a regression test too: reproduce the flaw as a failing test before fixing it.
See systematic-debugging for the investigation discipline. Structural cleanups
belong to the refactoring skill — do them in the REFACTOR step with the
tests green.
Final rule
Production code → a test exists and failed first
Otherwise → not TDD
Provenance
Adapted from obra/superpowers (MIT), © 2025 Jesse Vincent. See
THIRD_PARTY_NOTICES.md.