| name | clean-tests |
| description | Use when writing, fixing, editing, or refactoring TypeScript tests. Enforces Clean Code principles—fast tests, boundary coverage, one assert per test. |
| when_to_use | Also trigger on: slow or flaky tests, `test.skip`/`it.skip`/`.todo` without a clear reason, `test.only` left in committed code, tests that only cover the happy path, tests with multiple assertions about different concepts, missing boundary cases (empty arrays, off-by-one, page zero), or asks about "coverage gap" / "edge case".
|
Clean Tests
T1: Insufficient Tests
Test everything that could possibly break. Use coverage tools as a guide, not a goal.
test("divide", () => {
expect(divide(10, 2)).toBe(5);
});
test("divide normal", () => {
expect(divide(10, 2)).toBe(5);
});
test("divide by zero", () => {
expect(() => divide(10, 0)).toThrow(RangeError);
});
test("divide negative", () => {
expect(divide(-10, 2)).toBe(-5);
});
T2: Use a Coverage Tool
Coverage tools report gaps in your testing strategy. Don't ignore them.
vitest run --coverage
T3: Don't Skip Trivial Tests
Trivial tests document behavior and catch regressions. They're worth more than their cost.
test("user default role", () => {
const user = new User("Alice");
expect(user.role).toBe("member");
});
T4: An Ignored Test Is a Question About an Ambiguity
Don't use test.skip to hide problems. Either fix the test or delete it.
test.skip("async operation", () => {
});
test.skip("cache invalidation - requires Redis (see CONTRIBUTING.md)", () => {
});
T5: Test Boundary Conditions
Bugs congregate at boundaries. Test them explicitly.
test("pagination boundaries", () => {
const items = Array.from({ length: 100 }, (_, i) => i);
expect(paginate(items, 1, 10)).toEqual(items.slice(0, 10));
expect(paginate(items, 10, 10)).toEqual(items.slice(90, 100));
expect(paginate(items, 11, 10)).toEqual([]);
expect(() => paginate(items, 0, 10)).toThrow(RangeError);
expect(paginate([], 1, 10)).toEqual([]);
});
T6: Exhaustively Test Near Bugs
When you find a bug, write tests for all similar cases. Bugs cluster.
test("month boundaries", () => {
expect(lastDayOfMonth(2024, 1)).toBe(31);
expect(lastDayOfMonth(2024, 2)).toBe(29);
expect(lastDayOfMonth(2023, 2)).toBe(28);
expect(lastDayOfMonth(2024, 4)).toBe(30);
expect(lastDayOfMonth(2024, 12)).toBe(31);
});
T7: Patterns of Failure Are Revealing
When tests fail, look for patterns. They often point to deeper issues.
T8: Test Coverage Patterns Can Be Revealing
Look at which code paths are untested. Often they reveal design problems.
T9: Tests Should Be Fast
Slow tests don't get run. Keep unit tests under 100ms each.
test("user creation", async () => {
const db = await connectToDatabase();
const user = await db.createUser("Alice");
expect(user.name).toBe("Alice");
});
test("user creation", async () => {
const db = new InMemoryDatabase();
const user = await db.createUser("Alice");
expect(user.name).toBe("Alice");
});
Test Organization
F.I.R.S.T. Principles
- Fast: Tests should run quickly
- Independent: Tests shouldn't depend on each other
- Repeatable: Same result every time, any environment
- Self-Validating: Pass or fail, no manual inspection
- Timely: Written before or with the code, not after
One Concept Per Test
test("user", () => {
const user = new User("Alice", "alice@example.com");
expect(user.name).toBe("Alice");
expect(user.email).toBe("alice@example.com");
expect(user.isValid()).toBe(true);
user.activate();
expect(user.isActive).toBe(true);
});
test("user stores name", () => {
const user = new User("Alice", "alice@example.com");
expect(user.name).toBe("Alice");
});
test("user stores email", () => {
const user = new User("Alice", "alice@example.com");
expect(user.email).toBe("alice@example.com");
});
test("new user is valid", () => {
const user = new User("Alice", "alice@example.com");
expect(user.isValid()).toBe(true);
});
test("user can be activated", () => {
const user = new User("Alice", "alice@example.com");
user.activate();
expect(user.isActive).toBe(true);
});
Quick Reference
| Rule | Principle |
|---|
| T1 | Test everything that could break |
| T2 | Use coverage tools |
| T3 | Don't skip trivial tests |
| T4 | Ignored test = ambiguity question |
| T5 | Test boundary conditions |
| T6 | Exhaustively test near bugs |
| T7 | Look for patterns in failures |
| T8 | Check coverage when debugging |
| T9 | Tests must be fast (<100ms) |