·11 min read

Claude Code Unit Tests: Build a Test-Writing Skill

Turn Claude Code unit tests from one-off prompts into a reusable test-writing skill: conventions, coverage scripts, and a team rollout plan.

The fastest way to get consistently good Claude Code unit tests is to stop prompting for them and start encoding your testing conventions as a skill. A skill is a folder with a SKILL.md file that Claude Code loads whenever a task involves writing tests — so your framework choice, naming rules, mocking policy, and coverage expectations travel with every session instead of being retyped into chat and forgotten by the next one.

This guide builds that skill from scratch. You'll design the conventions worth encoding, write the SKILL.md step by step, add scripts that run coverage checks automatically, and roll the finished skill out to your whole team so everyone's AI writes tests the same way.

Why Claude Code Unit Tests Deserve a Skill, Not a One-Off Prompt

"Write tests for this" is one of the most common prompts developers type — and one of the least reliable. AI test generation is only as good as the context behind it, and a bare prompt carries almost none. Left to defaults, Claude Code will guess your framework, invent its own naming style, and mock aggressively enough that some tests verify nothing but the mocks.

The failure isn't that any single test is bad. It's that every session starts from zero:

  • Framework drift. One session produces Jest syntax, the next produces Vitest, because nothing pins the choice down.
  • Mock-everything tests. Without a mocking policy, the AI mocks the very function under test and the suite goes green while proving nothing.
  • Happy-path bias. Error branches, boundary inputs, and concurrency edges get skipped unless something forces them onto the checklist.
  • Inconsistency across teammates. Your prompt and your coworker's prompt encode different unwritten rules, so the test suite becomes an archaeology dig of styles.

You can fight this by pasting a long testing preamble into every session, but that's exactly the repetition skills exist to remove. Claude Code reads each skill's name and description at startup, then pulls in the full instructions only when the task matches — which means your test conventions cost almost nothing until the moment they're needed. If you're new to the mechanics, the step-by-step guide to creating a Claude skill covers how loading and triggering work in detail.

There's a second reason to formalize this: AI-written tests need more scrutiny than AI-written code, not less, because a plausible-looking test that asserts the wrong thing hides bugs instead of catching them. Our guide to testing AI-generated code digs into those failure modes; the skill you're about to build is how you turn its lessons into defaults.

Designing the Unit Testing Agent Skill

Before writing a line of markdown, make three design decisions. A unit testing agent skill is only useful if it answers the questions Claude would otherwise guess at:

DecisionQuestion it answersExample answer
ConventionsWhat framework, file layout, and imports?Vitest, test file next to source, explicit imports
Coverage strategyWhat must every test file cover?Happy path, every error path, boundary inputs
NamingHow do tests describe themselves?Behavior-first it strings, issue refs for regressions

Conventions are the mechanical rules: which runner, where test files live, how mocks are created and restored. These are the cheapest wins because they're unambiguous — the AI either follows them or it doesn't, and reviewers can spot violations instantly.

Coverage strategy is where most test-writing prompts are silent. Decide what "tested" means in your codebase and write it as an ordered checklist: documented behavior first, then every error path, then boundaries (empty collections, zero, negative numbers, unicode). If you practice regression-test-first bug fixing, say so explicitly — "write the failing test before the fix" is exactly the kind of instruction an agent follows when it's in context and ignores when it isn't.

Naming sounds cosmetic but drives quality. A rule like "it descriptions state behavior, not implementation" forces the AI to articulate what each test proves. If it can't phrase the behavior, the test probably shouldn't exist.

Keep the skill scoped to unit tests. Integration and end-to-end testing have different rules (real databases, no fake timers, different layout), and cramming them into one skill dilutes the trigger description. Ship writing-unit-tests first; add siblings later.

Building the SKILL.md Step by Step

Time to write the thing. Follow these steps in order.

Step 1: Scaffold the folder

mkdir -p .claude/skills/writing-unit-tests
touch .claude/skills/writing-unit-tests/SKILL.md

Project-level .claude/skills/ is the right home: test conventions belong to the repo, not to one developer's machine.

Step 2: Write a trigger-rich description

The frontmatter description is the only part of the skill Claude sees before deciding to load it, so pack it with the situations that should trigger it — writing tests, adding coverage, fixing bugs that need regression tests.

Step 3: Encode the conventions

Here's a complete, working example you can adapt:

---
name: writing-unit-tests
description: Use when writing or updating unit tests, adding coverage
  for new code, or fixing a bug that needs a regression test. Covers
  the test framework, file layout, naming, mocking policy, and
  coverage expectations for this repo.
---

# Writing unit tests

## Framework and layout

- Vitest with globals disabled — import `describe`, `it`, `expect` explicitly
- Test files live next to source: `parseInvoice.ts``parseInvoice.test.ts`
- One `describe` block per exported function or class

## What every test file must cover

1. The documented happy path
2. Every error path: thrown errors, rejected promises, error returns
3. Boundary inputs: empty arrays, zero, negative numbers, unicode strings
4. If fixing a bug: write the failing regression test FIRST, then the fix

## Naming

- `it` strings state behavior, not implementation:
  "rejects invoices with a negative total", never "works correctly"
- Regression tests cite the issue: "handles empty line items (#482)"

## Mocking policy

- Mock at system boundaries only: network, filesystem, clock, randomness
- NEVER mock the module under test or its pure helpers
- Use fake timers for time-dependent code; never sleep in a test
- Restore all mocks in afterEach

## Before you finish

Run `scripts/check-coverage.sh` from the skill folder and include its
output in your summary. If a changed file is under the threshold, add
tests — do not lower the threshold or add coverage-ignore comments.

Step 4: Add an anti-pattern list

The "NEVER" lines above do disproportionate work. Every team accumulates test smells — snapshot tests standing in for behavior tests, assertions on private state, order-dependent suites. Add each one you've been burned by as an explicit prohibition. Negative rules are the fastest way to steer ai test generation away from plausible-but-worthless output.

Step 5: Test the trigger before trusting it

Open a fresh Claude Code session and try prompts at different distances from the description: "add tests for the invoice parser," "there's a bug in date handling — fix it," "improve coverage in src/billing." The bug-fix prompt is the interesting one — it never says "test," but your description mentions regression tests, so the skill should still load and produce a failing test before the fix. If it doesn't trigger, enrich the description, not the body. For deeper frontmatter mechanics, see the SKILL.md format reference.

Adding Scripts: Coverage Checks and Test Runs

Instructions tell Claude what good tests look like; scripts let it verify its own work. Skills are folders, so ship the tooling alongside the markdown:

writing-unit-tests/
├── SKILL.md
└── scripts/
    └── check-coverage.sh

The coverage script gives Claude Code test coverage feedback it can act on within the same session — a number it must report, not a vibe:

#!/usr/bin/env bash
# Fails if a source file changed on this branch is under the threshold.
set -euo pipefail

THRESHOLD="${1:-80}"
CHANGED=$(git diff --name-only origin/main...HEAD -- 'src/**/*.ts' \
  | grep -v '\.test\.ts$' || true)

if [ -z "$CHANGED" ]; then
  echo "No source files changed — nothing to check."
  exit 0
fi

npx vitest run --coverage --coverage.reporter=json-summary --silent
node scripts/assert-coverage.mjs "$THRESHOLD" $CHANGED

Two details matter here. First, the script checks changed files, not the whole repo — a global threshold punishes whoever touches legacy code, while a changed-files threshold makes every AI-assisted change pull its own weight. Second, SKILL.md explicitly tells Claude to run the script and report its output, closing the loop: generate tests, measure, add more if short.

You can extend the same pattern with a focused test runner (map changed source files to their test files and run just those, so the feedback loop stays fast in large repos) or a mutation-testing spot check for critical modules. Because a published skill is a versioned folder package — docs, scripts, and templates together, up to 100 MB and 500 files per version — the tooling ships and updates with the instructions instead of drifting apart from them.

Claude Code Unit Tests at Scale: What Changes

What does a repo look like a few weeks after the skill lands? No invented metrics here — just the shifts you can verify in your own diffs and reviews.

Test diffs become predictable. Before the skill, reviewing AI-written tests meant re-litigating style in every PR: wrong runner idioms, vague it("works") strings, mocks wrapping the function under test. After, tests arrive in one shape — same layout, same naming grammar, same mock boundaries — and review attention shifts from style to substance: does this assertion actually pin the behavior?

Error paths stop being optional. The ordered coverage checklist is the single highest-leverage line in the skill. When "every error path" is item two of a list Claude walks through mechanically, the rejected-promise and malformed-input cases show up without anyone asking.

Bug fixes carry their own regression tests. Because the description mentions bug fixes and the body demands a failing test first, "fix the date bug" prompts start producing a test named after the issue plus the fix — instead of the fix alone.

The skill becomes your test-quality changelog. When a bug slips through anyway, the postmortem action item is one line in SKILL.md: add the missed case to the checklist or the anti-pattern list. The next session picks it up automatically. That feedback loop — incident to convention to enforcement — is something no one-off prompt can give you, because prompts don't accumulate.

Rolling the Skill Out to Your Team

A test-writing skill in one developer's .claude/skills/ folder fixes one developer's tests. The payoff comes when the whole team — and every AI tool they use — runs the same conventions.

Publish the skill to a registry and install it everywhere from a single source:

npm install -g @localskills/cli
localskills login
localskills publish            # from the writing-unit-tests folder
localskills install your-org/writing-unit-tests --target claude cursor windsurf

One published skill fans out into each tool's native format — .claude/skills/ for Claude Code, .cursor/rules for Cursor, .windsurf/rules/ for Windsurf — so teammates on different editors still share one testing standard. Versioning with rollback means updating the coverage checklist ships to everyone like a dependency bump, and download analytics show which teams actually installed it. Keep it private to your org, or publish it publicly if your conventions are worth sharing. The full workflow — namespaces, folders, access control — is covered in sharing Claude skills with your team and the localskills docs.

FAQ

Can Claude Code write unit tests without a skill?

Yes — it will happily generate tests from a bare prompt. The problem is consistency, not capability: without encoded conventions it guesses your framework, skips error paths, and over-mocks. A skill turns "sometimes great" into "reliably in-shape," which is what makes AI-written tests reviewable at scale.

How do I stop Claude Code from mocking everything in tests?

Write an explicit mocking policy into the skill: mock at system boundaries only (network, filesystem, clock, randomness) and never mock the module under test. Over-mocking is a default behavior, and defaults yield to clear, always-loaded instructions.

Do I need to invoke the skill manually every session?

No. Claude Code reads each skill's description at startup and loads the full instructions automatically when a task matches. That's why Step 5 above tests trigger phrasing — a description that mentions tests, coverage, and bug fixes fires on all three kinds of prompts.

Does a test-writing skill work in Cursor or Windsurf too?

The SKILL.md format is Claude Code's, but the conventions inside aren't Claude-specific. Publish once and install with --target cursor claude windsurf — the CLI writes each tool's native rules format from the same source, so the whole team stays aligned regardless of editor.


Ready to make good tests the default instead of the exception? Create a free localskills.sh account, publish your test-writing skill, and install it across every AI tool your team uses.

npm install -g @localskills/cli
localskills login
localskills publish