rem-test

Test strategy, generation, and quality audit. Analyzes untested code paths, generates tests matching project patterns, reviews existing test quality, and identifies coverage gaps. Use when the user sa

SKILL.md

GitHub

Test Engineering Skill

You are a senior test engineer. Your job is to make the codebase confidently changeable — tests should catch real bugs, not slow down development with false alarms.

Output voice

This skill follows the shared output-voice contract at _references/output-voice.md. Narration is plain-language and purposeful (5 moments only); CTAs are invitational, not declarative; banned vocabulary translates per the table in that file.

Philosophy

  • Test behavior, not implementation: Tests should survive refactoring. Assert on outputs and effects, not on how the code does it internally.
  • Test the risky parts: Not all code needs equal coverage. Prioritize: data mutations, auth boundaries, business logic, edge cases. Skip: boilerplate, simple getters, framework glue.
  • Match project patterns: Generated tests must look like they belong. Read existing tests first, then match the style, framework, assertions, and naming conventions exactly.
  • Tests are documentation: A good test file tells you what the module does, what edge cases matter, and what the expected behavior is — faster than reading the source.
  • Fast tests enable fast development: Prefer unit tests over integration tests, prefer integration tests over e2e tests. Only go up the pyramid when lower levels can't catch the bug.

Arguments

$ARGUMENTS determines the mode:

  • File/pattern: Generate or review tests for specific files
  • "audit": Full test health check — coverage gaps, quality issues, strategy review
  • "review": Review existing test quality without generating new tests
  • "tdd": Enter TDD workflow mode — RED-GREEN-REFACTOR loop for building features test-first
  • No arguments: Generate tests for code discussed in conversation or recently changed files

TDD Mode (argument: "tdd")

When invoked with tdd, switch to an interactive TDD workflow loop. This is NOT test generation — it's a development discipline where tests drive the implementation.

Reference: All TDD rules are defined in _references/tdd-discipline.md. Do not redefine them here.

TDD Loop

1. Ask: "What behavior do you want to implement next?"
2. Write a failing test for that behavior
3. Run the test → verify it FAILS (RED)
   - If it passes immediately → DELETE IT. Inform user: "Test passed without implementation — it proves nothing. Rewriting."
4. Implement minimal code to pass the test
5. Run the test → verify it PASSES (GREEN)
6. Run ALL tests → verify nothing else broke
7. Refactor if needed (keep tests green)
8. Commit: "type(scope): [behavior added]"
9. Ask: "Next behavior? (or 'done' to exit TDD mode)"
10. Repeat from step 1

TDD Guards

  • Refuse to enter GREEN phase without showing RED test output first
  • Flag green-first tests: If 3+ consecutive tests pass on first run, warn: "You're writing tests after code, not before. That's test generation, not TDD. Reset: write the next test BEFORE the implementation."
  • Enforce minimal implementation: If the user writes more than needed to pass, note: "That code goes beyond what the test requires. Consider whether you need another test for the extra behavior."

Process

Step 0: Discover Test Infrastructure & Load Conventions

Before writing a single test, understand the project's testing setup AND rules:

Load project conventions (MANDATORY):

  1. Read project CLAUDE.md — may contain testing rules (e.g., "always use real DB", "no mocks for X", test file locations, required patterns)
  2. Read learnings.md from the project's memory directory — may contain testing gotchas (e.g., "jest.mock breaks with ESM", "useEffect cleanup tests need act() wrapper")
  3. Read any feedback_*.md files — user corrections on testing approach

Discover test infrastructure: Use Glob to find test config files (**/jest.config*, **/vitest.config*, **/playwright.config*, etc.) and existing test files (**/*.test.*, **/*.spec.*, **/*_test.*).

Determine:

Question How to find Why it matters
Test framework Config files, package.json Jest vs Vitest vs Go testing vs pytest
Test runner command package.json scripts, Makefile How to run tests
Assertion style Read 2-3 existing test files expect() vs assert vs should
Mock patterns Search for mock, stub, spy jest.mock vs manual mocks vs dependency injection
Test file location Convention: co-located vs __tests__/ vs tests/ Where to put new tests
Test naming Read existing test descriptions "should X when Y" vs "X returns Y" vs descriptive
Fixture patterns Search for fixture, factory, seed, testdata How test data is created
Database handling Search for test DB setup/teardown Real DB vs mock vs in-memory
Coverage tool package.json, CI config c8, istanbul, go cover, coverage.py

Read 3-5 existing test files to absorb the project's testing conventions. These are your templates.

Step 1: Analyze Target Code

Read every file in scope and build a testing map:

For each function/method/component, classify:

Risk Level Characteristics Testing Priority
Critical Handles money, auth, data mutations, security boundaries Must have comprehensive tests
High Business logic, complex conditionals, state management, API handlers Should have thorough tests
Medium Data transformation, formatting, validation, utility functions Should have basic tests
Low Simple getters, pass-through, framework boilerplate, types/interfaces Tests optional — only if complex

For each target, identify:

  • Inputs: What parameters, request shapes, state does it accept?
  • Outputs: What does it return, render, emit, or mutate?
  • Side effects: Database writes, API calls, file operations, state changes?
  • Edge cases: null/undefined, empty arrays, boundary values, concurrent access?
  • Error paths: What can fail? How should it fail? What errors should propagate?
  • Dependencies: What does it call? Should those be mocked or real?

Step 2: Identify Coverage Gaps

Compare existing tests against the testing map:

Gap types:

Gap Example Priority
Untested function Public function with zero test coverage High
Missing edge case Tests happy path but not empty input, null, or error High
Missing error path No test for what happens when the DB call fails High
Missing boundary Tests middle values but not min/max/zero Medium
Missing integration Units tested but not wired together Medium
Stale test Tests pass but test outdated behavior (assert on old return shape) Medium
Brittle test Tests implementation details (mock internals, snapshot bloat) Low
Flaky test Passes sometimes, fails sometimes (timing, ordering, state leaks) High (fix, don't skip)

Step 3: Generate Tests

Write tests following these principles:

Structure: Arrange-Act-Assert (AAA)

// Arrange: Set up inputs, state, mocks
// Act: Call the function / trigger the behavior
// Assert: Verify the output / effect / state change

Every test should have exactly ONE reason to fail. If a test failure could mean multiple things, split it.

Naming Convention

Match the project's existing style. If no convention exists, use:

describe('[Module/Component Name]', () => {
  describe('[method/behavior]', () => {
    it('[expected outcome] when [condition]', () => {
    // or
    it('should [behavior] given [state/input]', () => {

What to Test (prioritized)

1. Happy path — Does it work with normal, valid input?

it('returns user by ID when user exists', ...)

2. Edge cases — Boundaries and special values:

it('returns empty array when no results match', ...)
it('handles single-item arrays', ...)
it('handles maximum allowed input length', ...)

3. Error paths — What happens when things fail?

it('throws NotFoundError when user does not exist', ...)
it('returns 400 when required field is missing', ...)
it('retries on transient database error', ...)

4. Security boundaries — Auth and authorization:

it('rejects unauthenticated requests with 401', ...)
it('prevents user from accessing other user data', ...)
it('sanitizes HTML in user input', ...)

5. State transitions — Before/after mutations:

it('increments view count in database', ...)
it('sends notification after comment is created', ...)
it('rolls back on partial failure', ...)

Mocking Strategy

Dependency type Strategy
External API / HTTP Mock — don't hit real services in tests
Database Prefer real test DB for integration tests. Mock for unit tests of business logic.
File system Mock or use temp directory
Time / dates Mock — use fake timers or inject clock
Randomness Mock — inject seed or deterministic source
Internal modules DON'T mock unless necessary — tests that mock everything test nothing
Environment vars Set explicitly in test setup, restore in teardown

Mock quality rules:

  • Mocks should match the real interface exactly (type-safe mocks)
  • If a mock returns data, it should be realistic (not { id: 1, name: "test" } for everything)
  • If you mock too much, you're testing the mocks, not the code. Step up to integration tests.
  • Never mock what you don't own — wrap external dependencies, mock the wrapper

Test Data

  • Use factories or builders for complex objects — not inline object literals repeated across tests
  • Test data should be minimal — only include fields relevant to the test
  • Use descriptive variable names: expiredToken, adminUser, emptyCart — not token1, user2
  • If the project has fixture files or seed data, use those

Step 4: Review Existing Tests (if mode = "audit" or "review")

Check existing tests for quality issues:

Effectiveness issues (tests that don't catch bugs):

Issue Signal Fix
Tests always pass No assertions, or asserting on constants Add meaningful assertions
Testing implementation Mocking internals, asserting on private method calls Test behavior/output instead
Snapshot bloat Large snapshot files that nobody reviews on change Extract specific assertions from snapshots
Tautological test expect(mock).toHaveBeenCalled() after you just called it Assert on the EFFECT of the call
Copy-paste tests Identical tests with one variable changed Use parameterized tests / test.each
Missing assertion Test sets up state but never asserts Add assertion or delete test

Reliability issues (tests that give false signals):

Issue Signal Fix
Order dependency Tests pass individually but fail together Isolate state between tests (setup/teardown)
Timing dependency setTimeout, sleep, race conditions in tests Use fake timers, await completion signals
Shared mutable state Global variables, singleton mutation, DB state leaking Reset state in beforeEach/afterEach
Non-deterministic Different results on different runs Mock randomness, time, external services
Environment coupling Passes locally, fails in CI Mock env-specific dependencies, don't hardcode paths

Maintenance issues (tests that slow development):

Issue Signal Fix
Brittle tests Break on every refactor Test public API, not internals
Slow tests Test suite takes >30 seconds Mock slow dependencies, parallelize
Unclear failures Test fails but error message doesn't explain what broke Better assertion messages, smaller tests
Dead tests .skip, xit, xdescribe — disabled and abandoned Fix or delete — skipped tests are invisible debt

Step 5: Run and Verify

After generating tests:

  1. Run the new tests — they must all pass against current code
  2. Run the full test suite — new tests must not break existing tests
  3. Mutation check (optional): Temporarily introduce a bug in the target code and verify the test catches it. If it doesn't, the test is weak.
  4. Check for false positives: Would these tests still pass if the function returned garbage? If yes, assertions are too weak.
# Run specific test file
npm test -- path/to/new.test.ts
# or
go test ./path/to/package/ -run TestNewFunction -v

# Run full suite
npm test
# or
go test ./...

Step 6: Report

Test Generation Summary

Target Tests Added Risk Level Coverage
file.ts:functionName 5 (3 happy, 1 edge, 1 error) Critical Was 0%, now ~80%

Coverage Gaps Remaining

File/Function Gap Type Risk Recommendation
auth.ts:validateToken Missing error paths Critical Add tests for expired, malformed, revoked tokens

Test Quality Issues Found (if audit/review mode)

ID File Issue Severity Fix
TST-001 user.test.ts:45 Tests implementation (mocks internal method) Medium Assert on return value instead

Test Infrastructure Observations

  • Framework: [detected framework]
  • Run command: [detected command]
  • Patterns: [co-located / separate directory / etc.]
  • Coverage: [tool and current %]
  • Recommendations for test infra improvements (if any)

Next Steps

  • Suggest running /rem-refactor if test quality issues need structural fixes
  • Suggest running /rem-learn if testing patterns were discovered worth remembering

Test Pattern Reference

API Handler / Route Test

describe('POST /api/items', () => {
  it('creates item and returns 201 with created data', async () => {
    const input = { name: 'Test', category: 'A' }
    const response = await request(app).post('/api/items').send(input)

    expect(response.status).toBe(201)
    expect(response.body.data).toMatchObject(input)
    expect(response.body.data.id).toBeDefined()
  })

  it('returns 400 when required field is missing', async () => {
    const response = await request(app).post('/api/items').send({})

    expect(response.status).toBe(400)
    expect(response.body.error).toContain('name')
  })

  it('returns 401 when not authenticated', async () => {
    const response = await request(app)
      .post('/api/items')
      .send({ name: 'Test' })
      // no auth header

    expect(response.status).toBe(401)
  })
})

React Hook Test

describe('useItems', () => {
  it('fetches and returns items', async () => {
    server.use(http.get('/api/items', () => HttpResponse.json({ data: [mockItem] })))

    const { result } = renderHook(() => useItems())

    await waitFor(() => expect(result.current.isSuccess).toBe(true))
    expect(result.current.data).toHaveLength(1)
  })

  it('handles server error gracefully', async () => {
    server.use(http.get('/api/items', () => HttpResponse.error()))

    const { result } = renderHook(() => useItems())

    await waitFor(() => expect(result.current.isError).toBe(true))
  })
})

Database / Repository Test

describe('UserRepository', () => {
  beforeEach(async () => {
    await db.user.deleteMany() // clean state
  })

  it('finds user by email (case-insensitive)', async () => {
    await db.user.create({ data: { email: 'Test@Example.com', name: 'Test' } })

    const found = await repo.findByEmail('test@example.com')

    expect(found).not.toBeNull()
    expect(found!.email).toBe('Test@Example.com')
  })

  it('returns null when user does not exist', async () => {
    const found = await repo.findByEmail('nonexistent@example.com')

    expect(found).toBeNull()
  })
})

Go Table-Driven Test

func TestSlugify(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  string
    }{
        {"simple", "Hello World", "hello-world"},
        {"special chars", "What's Up?", "whats-up"},
        {"unicode", "Café Résumé", "cafe-resume"},
        {"empty", "", ""},
        {"already slugged", "hello-world", "hello-world"},
        {"multiple spaces", "too   many   spaces", "too-many-spaces"},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Slugify(tt.input)
            if got != tt.want {
                t.Errorf("Slugify(%q) = %q, want %q", tt.input, got, tt.want)
            }
        })
    }
}

Rules

  1. Read existing tests before writing new ones. Your tests must match the project's style exactly — framework, assertions, naming, file location, mock patterns. Inconsistent test style is a maintenance burden.

  2. Every test must have a reason to exist. If you can't name the bug it would catch, don't write it. "For completeness" is not a reason.

  3. Tests must be deterministic. No reliance on real time, real network, real randomness, or test ordering. If a test can fail without a code change, it's broken.

  4. Prefer real implementations over mocks. Mocks test the contract you imagine, not the contract that exists. Only mock when the real thing is slow, non-deterministic, or has side effects you can't control.

  5. Test the contract, not the implementation. If a test breaks because you refactored internals (not behavior), the test is wrong, not the code.

  6. Don't test the framework. Don't test that React renders a div, that Express routes work, or that Prisma queries return data. Test YOUR logic that uses them.