Developer skill testing automation quality

Skill: test writer

A skill that reads a source file and generates tests. Detects the language, framework, and existing patterns to match your style.

Writing tests is one of those tasks that everyone agrees is important and nobody wants to do at 4pm on a Friday. The work itself isn’t hard. It’s just repetitive: read the function, think about the inputs, write the happy path, think about the edge cases, write those, check that you matched the project’s testing style. Rinse and repeat for every file.

This skill automates the tedious parts. Point it at a source file, and it reads the code, finds your existing tests for style reference, generates tests that cover the main paths and edge cases, and then runs them to make sure they actually pass. You still review what it wrote. But you start from a working test file instead of a blank one.

The skill file

Create this file at .claude/skills/write-tests.md in your project:

# Skill: test writer

## Description

Generate tests for a given source file. Detects the programming
language, finds the project's testing framework, studies existing
test files for style patterns, and writes tests that match.

Invoke this skill with: /write-tests <file-path>
Or by asking: "Write tests for src/utils/parser.ts"

The file path argument is required. If not provided, ask for it.

## Steps

### 1. Read the source file

Read the full contents of the specified file. Identify:

- The programming language (from the file extension and content)
- Every exported function, class, or method
- Every public method on exported classes
- The function signatures: parameter names, types, return types
- Any dependencies the functions import or require

If the file doesn't exist, tell the developer and stop.
If the file has no testable exports (e.g., it's a type definition
file or a config file with no logic), explain why and stop.

### 2. Detect the testing framework

Look for the project's testing setup. Check these locations in order:

**For JavaScript/TypeScript:**

- `vitest.config.*` or `vite.config.*` with test config → Vitest
- `jest.config.*` or `"jest"` in package.json → Jest
- `package.json` dependencies containing `mocha` → Mocha + Chai
- If none found, default to Vitest

**For Python:**

- `pytest.ini`, `pyproject.toml` with `[tool.pytest]`, or
  `conftest.py` in the project → pytest
- If none found, default to pytest

**For Go:**

- Go uses the standard `testing` package. No detection needed.

**For Rust:**

- Rust uses built-in `#[cfg(test)]` modules. No detection needed.

**For other languages:**

- Check for common test runner config files.
- If you can't determine the framework, ask the developer.

Also note the test runner command so you can run the tests later:

- Vitest: `npx vitest run`
- Jest: `npx jest`
- pytest: `python -m pytest` or `pytest`
- Go: `go test ./...`
- Rust: `cargo test`

### 3. Find existing test patterns

Search the project for existing test files. Look in:

- `__tests__/` directories
- `test/` or `tests/` directories
- Files named `*.test.*` or `*.spec.*` colocated with source
- For Go: `*_test.go` files
- For Rust: `mod tests` blocks in source files

Read 2-3 existing test files (preferably ones testing similar code)
and note the project's testing patterns:

- **Import style:** How do they import the test framework and the
  code under test? Named imports, default imports, relative paths?
- **Describe/it structure vs flat test functions:** Do they nest
  describes? Use `it` or `test`?
- **Naming conventions:** "should do X", "does X when Y",
  "test_function_name_condition", etc.
- **Setup patterns:** beforeEach/afterEach, test fixtures, factory
  functions, mocks?
- **Assertion style:** expect().toBe(), assert.equal(), t.Equal()?
- **Mock patterns:** Do they use vi.mock, jest.mock, unittest.mock,
  manual stubs?
- **File naming:** `foo.test.ts` next to `foo.ts`? Or
  `__tests__/foo.test.ts`?
- **File location:** Where do test files live relative to source?

If no existing tests are found, use the testing framework's
standard conventions.

### 4. Plan the tests

For each exported function or public method, plan test cases:

**Happy path tests (required for every function):**

- Call with typical, valid inputs
- Verify the expected return value

**Edge case tests (add where applicable):**

- Empty inputs (empty string, empty array, null, undefined)
- Boundary values (zero, negative numbers, max int, very long
  strings)
- Single-element collections
- Inputs at type boundaries

**Error path tests (add where applicable):**

- Invalid inputs that should throw or return errors
- Missing required parameters
- Inputs that violate documented constraints

**Integration-style tests (only if the function has dependencies):**

- Mock external dependencies (database, HTTP, file system)
- Test that the function calls its dependencies correctly
- Test behavior when dependencies fail

Do not generate tests for:

- Private/internal functions (unless the language exposes them
  to the test module, like Rust's `mod tests`)
- Trivial getters/setters with no logic
- Functions that only re-export from another module

### 5. Write the tests

Generate the complete test file. Follow these rules:

- Match the import style, naming conventions, describe structure,
  and assertion patterns you found in step 3.
- Put the test file in the same location pattern as existing tests
  (colocated or in a test directory).
- Include a brief comment at the top: `// Tests for <source-file>`
- Group tests by function: one describe block (or equivalent) per
  function being tested.
- Give each test a clear name that states the expected behavior,
  not the implementation detail.
- Keep tests independent. No test should depend on another test's
  state or execution order.
- Use realistic test data, not generic placeholders like "foo" and
  "bar" (unless testing string-agnostic behavior).
- For mocked dependencies, set up mocks in the smallest scope
  possible (per-test, not per-file, unless the pattern in step 3
  says otherwise).

Write the file to disk at the appropriate path.

### 6. Run the tests

Run the test suite using the command identified in step 2. Run
only the new test file, not the entire suite:

- Vitest: `npx vitest run <test-file>`
- Jest: `npx jest <test-file>`
- pytest: `python -m pytest <test-file> -v`
- Go: `go test -run <TestFunctionPattern> <package>`
- Rust: `cargo test <test-name-pattern>`

### 7. Fix failures

If any tests fail:

- Read the error output carefully.
- Determine whether the test is wrong (bad assertion, wrong mock
  setup) or the source code has a bug.
- If the test is wrong, fix the test and re-run. Repeat up to
  3 times.
- If the source code appears to have a bug, do NOT fix the source.
  Instead, add a comment to the failing test:
  `// FIXME: This test reveals a potential bug — [description]`
  and mark the test as skipped/pending with an explanation.

After all tests pass (or are explicitly skipped with explanations),
show the developer the results.

### 8. Report results

Show the developer:

```
## Test generation results

**Source file:** <path>
**Test file:** <path>
**Tests written:** <count>
**Tests passing:** <count>
**Tests skipped:** <count> (with reasons if any)

### Coverage

- function_name: [X tests] — happy path, edge cases, error handling
- another_function: [Y tests] — happy path, boundary values
...

### Notes

[Any observations: functions that were hard to test, dependencies
that needed complex mocking, potential bugs found, suggestions
for improving testability]
```

## Rules

- Always match the existing project style. Your tests should look
  like a human on the team wrote them.
- Never modify the source file being tested. Tests adapt to the
  code, not the other way around.
- If you can't determine the testing framework, ask. Do not guess
  wrong and generate incompatible tests.
- Run the tests before reporting success. "I wrote tests" is not
  the same as "I wrote tests that pass."
- Keep test files focused. One test file per source file. Don't
  combine tests for multiple source files.

How to install it

  1. Create the directory: mkdir -p .claude/skills/
  2. Save the file above as .claude/skills/write-tests.md
  3. Invoke it by typing /write-tests src/utils/parser.ts in Claude Code.

Like any skill file, you can commit it to your repository for the team or keep it in ~/.claude/skills/ for personal use.

Example usage

You’ve just written a utility module and need tests. You type /write-tests src/utils/validation.ts and the agent goes to work.

First it reads your file and finds three exported functions:

// src/utils/validation.ts
export function isValidEmail(email: string): boolean { ... }
export function parseAge(input: string): number | null { ... }
export function sanitizeUsername(name: string): string { ... }

It finds your project uses Vitest with colocated test files (*.test.ts next to source). It reads src/utils/format.test.ts to learn your test patterns: you use describe/it blocks, expect assertions, and name tests with “should” phrasing.

Then it generates and writes the test file:

// Tests for src/utils/validation.ts
import { describe, it, expect } from "vitest";
import { isValidEmail, parseAge, sanitizeUsername } from "./validation";

describe("isValidEmail", () => {
  it("should return true for a standard email address", () => {
    expect(isValidEmail("[email protected]")).toBe(true);
  });

  it("should return true for an email with subdomains", () => {
    expect(isValidEmail("[email protected]")).toBe(true);
  });

  it("should return false for a string without an @ symbol", () => {
    expect(isValidEmail("not-an-email")).toBe(false);
  });

  it("should return false for an empty string", () => {
    expect(isValidEmail("")).toBe(false);
  });

  it("should return false for an email with spaces", () => {
    expect(isValidEmail("has [email protected]")).toBe(false);
  });
});

describe("parseAge", () => {
  it("should parse a valid integer string", () => {
    expect(parseAge("25")).toBe(25);
  });

  it("should return null for a non-numeric string", () => {
    expect(parseAge("twenty")).toBeNull();
  });

  it("should return null for an empty string", () => {
    expect(parseAge("")).toBeNull();
  });

  it("should return null for a negative number", () => {
    expect(parseAge("-5")).toBeNull();
  });

  it("should handle zero", () => {
    expect(parseAge("0")).toBe(0);
  });
});

describe("sanitizeUsername", () => {
  it("should trim whitespace from both ends", () => {
    expect(sanitizeUsername("  alice  ")).toBe("alice");
  });

  it("should convert to lowercase", () => {
    expect(sanitizeUsername("Alice")).toBe("alice");
  });

  it("should remove special characters", () => {
    expect(sanitizeUsername("alice!@#bob")).toBe("alicebob");
  });

  it("should handle an already clean username", () => {
    expect(sanitizeUsername("alice")).toBe("alice");
  });
});

It runs the tests, they all pass, and it reports:

## Test generation results

**Source file:** src/utils/validation.ts
**Test file:** src/utils/validation.test.ts
**Tests written:** 14
**Tests passing:** 14
**Tests skipped:** 0

### Coverage

- isValidEmail: 5 tests — valid formats, missing @, empty, spaces
- parseAge: 5 tests — valid int, non-numeric, empty, negative, zero
- sanitizeUsername: 4 tests — trim, lowercase, special chars, clean input

### Notes

The isValidEmail function uses a regex that may not handle all
RFC 5322 edge cases (like quoted local parts). Consider whether
that level of strictness is needed for your use case.

How it works

The skill follows a pattern that applies well beyond test generation: observe the project, match its conventions, do the work, verify the result.

Style detection is the most important step. Steps 2 and 3 spend significant effort finding and reading existing tests. This is not optional. Tests that use a different framework, different assertion style, or different file location will annoy the team more than having no tests at all. The agent reads real examples from your project and copies the patterns. This is the same principle described in Writing effective skill instructions: give the agent concrete reference points, not abstract guidelines.

The test plan comes before the test code. Step 4 exists to make the agent think about what to test before writing anything. Without this step, agents tend to generate tests for whatever is easiest rather than what’s most valuable. The explicit categories (happy path, edge cases, error paths) serve as a checklist.

Running the tests is non-negotiable. Step 6 is what separates this skill from a simple “generate some tests” prompt. Generated tests that don’t compile or don’t pass are worse than useless because they waste time. The skill requires the agent to run the tests and fix failures before reporting success. If something can’t be fixed (because the source code has a bug), the skill has a protocol for that too: skip the test, explain why, and move on.

The skill never modifies source code. This is a deliberate constraint. A test-writing skill that also “fixes” the source code to make tests pass is dangerous. The developer needs to see what the tests expect and decide whether the code or the test is wrong. The rule in the skill definition makes this boundary explicit. As covered in How to design AI agent skills, constraints like this belong in the skill file, not in the developer’s head.

Customizing it

Add your testing conventions. If your team has specific rules (like “always use test instead of it” or “put integration tests in a separate directory”), add them to the rules section or to step 5.

Adjust coverage depth. The default skill generates happy path, edge case, and error tests. If you only want happy path tests as a starting point, simplify step 4. If you want exhaustive coverage, add categories like “concurrency tests” or “performance regression tests.”

Add snapshot testing. For UI components, you might want the skill to generate snapshot tests. Add a check in step 4: “If the function returns JSX or HTML, add a snapshot test using the framework’s snapshot capability.”

Customize the mock strategy. Some teams prefer dependency injection over mocking. Others use specific mock libraries. Update step 5 with your preferred approach. For example: “Never use vi.mock() for module-level mocking. Instead, pass dependencies as function parameters and provide test doubles.”

Add mutation testing. After the tests pass, you could add a step that runs a mutation testing tool (like Stryker for JS or mutmut for Python) to check whether the tests actually catch bugs. This is advanced, but it turns the skill into a real quality gate.

Support multiple files. The skill as written takes a single file path. If you often need to generate tests for a whole directory, add a variant that accepts a directory path and iterates over all testable files in it. Be careful with this though. Generating tests for 20 files at once is slow and the results are harder to review. Batching 3-5 files at a time is more practical.