AgentSkillsCN

clean-tests

在编写、修复、编辑或重构 TypeScript 测试时使用。严格遵循 Clean Code 原则——测试速度快、边界覆盖充分、每项测试仅包含一个断言。

SKILL.md
--- frontmatter
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.

Clean Tests

T1: Insufficient Tests

Test everything that could possibly break. Use coverage tools as a guide, not a goal.

typescript
// Bad - only tests happy path
test("divide", () => {
    expect(divide(10, 2)).toBe(5);
});

// Good - tests edge cases too
test("divide normal", () => {
    expect(divide(10, 2)).toBe(5);
});

test("divide by zero throws", () => {
    expect(() => divide(10, 0)).toThrow();
});

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.

bash
# Run with coverage (Jest)
npx jest --coverage

# Run with coverage (Vitest)
npx vitest --coverage

# Aim for meaningful coverage, not 100%

T3: Don't Skip Trivial Tests

Trivial tests document behavior and catch regressions. They're worth more than their cost.

typescript
// Worth having - documents expected behavior
test("user default role", (): void => {
    const user: User = new User({ name: "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.

typescript
// Bad - hiding a problem
test.skip("async operation", () => {
    // flaky, fix later
});

// Good - either fix it or document why it's skipped
test.skip("cache invalidation", () => {
    // Requires Redis, see CONTRIBUTING.md for setup
});

T5: Test Boundary Conditions

Bugs congregate at boundaries. Test them explicitly.

typescript
describe("pagination boundaries", (): void => {
    const items: number[] = Array.from(
        { length: 100 },
        (_: unknown, index: number): number => index
    );

    test("first page", () => {
        expect(paginate(items, { page: 1, size: 10 })).toEqual(items.slice(0, 10));
    });

    test("last page", () => {
        expect(paginate(items, { page: 10, size: 10 })).toEqual(items.slice(90, 100));
    });

    test("beyond last page", () => {
        expect(paginate(items, { page: 11, size: 10 })).toEqual([]);
    });

    test("page zero throws", () => {
        expect(() => paginate(items, { page: 0, size: 10 })).toThrow();
    });

    test("empty list", () => {
        expect(paginate([], { page: 1, size: 10 })).toEqual([]);
    });
});

T6: Exhaustively Test Near Bugs

When you find a bug, write tests for all similar cases. Bugs cluster.

typescript
// Found bug: off-by-one in date calculation
// Now test ALL date boundaries
describe("month boundaries", () => {
    test("January", () => expect(lastDayOfMonth(2024, 1)).toBe(31));
    test("leap year February", () => expect(lastDayOfMonth(2024, 2)).toBe(29));
    test("non-leap February", () => expect(lastDayOfMonth(2023, 2)).toBe(28));
    test("30-day month", () => expect(lastDayOfMonth(2024, 4)).toBe(30));
    test("December", () => expect(lastDayOfMonth(2024, 12)).toBe(31));
});

T7: Patterns of Failure Are Revealing

When tests fail, look for patterns. They often point to deeper issues.

typescript
// If all async tests fail intermittently,
// the problem isn't the tests—it's the async handling

T8: Test Coverage Patterns Can Be Revealing

Look at which code paths are untested. Often they reveal design problems.

typescript
// If you can't easily test a function, it probably does too much
// Refactor for testability

T9: Tests Should Be Fast

Slow tests don't get run. Keep unit tests under 100ms each.

typescript
// Bad - hits real database
test("user creation", async () => {
    const db = await connectToDatabase();  // Slow!
    const user = await db.createUser("Alice");
    expect(user.name).toBe("Alice");
});

// Good - uses mock or in-memory
test("user creation", (): void => {
    const db: InMemoryDatabase = new InMemoryDatabase();
    const user: User = 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

typescript
// Bad - testing multiple things
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);
});

// Good - one concept each
test("user stores name", (): void => {
    const user: User = new User("Alice", "alice@example.com");
    expect(user.name).toBe("Alice");
});

test("user stores email", (): void => {
    const user: User = new User("Alice", "alice@example.com");
    expect(user.email).toBe("alice@example.com");
});

test("new user is valid", (): void => {
    const user: User = new User("Alice", "alice@example.com");
    expect(user.isValid()).toBe(true);
});

test("user can be activated", (): void => {
    const user: User = new User("Alice", "alice@example.com");
    user.activate();
    expect(user.isActive).toBe(true);
});

Quick Reference

RulePrinciple
T1Test everything that could break
T2Use coverage tools
T3Don't skip trivial tests
T4Ignored test = ambiguity question
T5Test boundary conditions
T6Exhaustively test near bugs
T7Look for patterns in failures
T8Check coverage when debugging
T9Tests must be fast (<100ms)