test-driven-development — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited test-driven-development (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Write test first. Watch it fail. Write minimal code to pass. Refactor.
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
NO BEHAVIOR-CHANGING PRODUCTION CODE WITHOUT A FAILING TEST FIRSTWrote code before test? Delete it completely. Implement fresh from tests.
Refactoring is exempt: The refactor step changes structure, not behavior. Tests stay green throughout. No new failing test required.
RED ──► Verify Fail ──► GREEN ──► Verify Pass ──► REFACTOR ──► Verify Pass ──► Next RED
│ │ │
▼ ▼ ▼
Wrong failure? Still failing? Broke tests?
Fix test, retry Fix code, retry Fix, retryWrite one minimal test for one behavior.
Good example:
test('retries failed operations 3 times', async () => {
let attempts = 0;
const operation = async () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retryOperation(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});Clear name, tests real behavior, asserts observable outcome
Bad example:
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});Vague name, asserts only call count without verifying outcome, tests mock mechanics not behavior
Requirements: One behavior. Clear name. Real code (mocks only if unavoidable).
MANDATORY. Never skip.
npm test path/to/test.test.tsTest must go red for the right reason. Acceptable RED states:
Not acceptable: Runtime setup errors, import failures, environment issues.
Test passes immediately? You're testing existing behavior—fix test. Test errors for wrong reason? Fix error, re-run until it fails correctly.
Write simplest code to pass the test.
Good example:
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');
}Just enough to pass
Bad example:
async function retryOperation<T>(
fn: () => Promise<T>,
options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; }
): Promise<T> { /* YAGNI */ }Over-engineered beyond test requirements
Write only what the test demands. No extra features, no "improvements."
MANDATORY.
npm test path/to/test.test.tsConfirm: Test passes. All other tests still pass. Output pristine (no errors, warnings).
Test fails? Fix code, not test. Other tests fail? Fix now before continuing.
After green only: Remove duplication. Improve names. Extract helpers.
Keep tests green throughout. Add no new behavior.
Next failing test for next behavior.
Minimal: One thing per test. "and" in name? Split it. ❌ test('validates email and domain and whitespace')
Clear: Name describes behavior. ❌ test('test1')
Shows intent: Demonstrates desired API usage, not implementation details.
Bug: Empty email accepted
RED:
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});Verify RED:
$ npm test
FAIL: expected 'Email required', got undefinedGREEN:
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}Verify GREEN:
$ npm test
PASSREFACTOR: Extract validation helper if pattern repeats.
Any of these means delete code and restart with TDD:
| Problem | Solution |
|---|---|
| Don't know how to test | Write the API you wish existed. Write assertion first. |
| Test too complicated | Design too complicated. Simplify the interface. |
| Must mock everything | Code too coupled. Introduce dependency injection. |
| Test setup huge | Extract helpers. Still complex? Simplify design. |
The Iron Law ("delete and restart") applies to new code you wrote without tests. For inherited code with no tests, use characterization tests:
Characterization tests lock down existing behavior so you can refactor safely. They're the on-ramp, not a permanent state.
Tests must be deterministic. Ban these in unit tests:
vi.useFakeTimers(), jest.useFakeTimers())Flaky test? Fix or delete. Flaky tests erode trust in the entire suite.
Bug found? Write failing test reproducing it first. Then follow TDD cycle. Test proves fix and prevents regression.
Before diving into the cycle, spend 2 minutes listing the next 3-10 tests you expect to write. This prevents local-optimum design where early tests paint you into a corner.
Example test list for a retry function:
Work through the list in order. Add/remove tests as you learn.
When writing tests involving mocks, dependencies, or test utilities: See references/testing-anti-patterns.md for common pitfalls including testing mock behavior and adding test-only methods to production classes.
For detailed rebuttals to common objections ("I'll test after", "deleting work is wasteful", "TDD is dogmatic"): See references/tdd-philosophy.md
Production code exists → test existed first and failed first
Otherwise → not TDD~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.