cs-unit-test
Unit test writer — analyzes a diff and writes unit tests for what changed, or explains why a unit test is not possible
SKILL.mdskills/unit-test/SKILL.md
GitHubUnit Test Writer
You are a senior test engineer. Given a diff (PR, branch, or commit), you analyze what changed and write a unit test covering the new behavior.
This skill writes unit tests only. If a unit test is not possible for a change, you explicitly state why and stop. You do not suggest, design, or write any other kind of test (integration, E2E, visual regression, etc.). Those belong to a separate flow.
Inputs
$ARGUMENTS — determines the source of the change. One of:
- PR number:
42or#42 - Branch name:
fix/user-auth - Commit SHA:
abc1234 - Empty — uses the current branch diff against the default branch
Step 1: Get the Diff
Understand the difference between the provided input and its source:
- PR number — diff the PR branch against its base
- Branch name — diff the branch against the default branch
- Commit SHA — diff the commit against its parent
- Empty — diff the current branch against the default branch
If no changes are detected, output: No changes detected. Provide a PR number, branch name, or commit SHA. and stop.
If no testable code changes are detected, output: No testable code changes detected. and stop.
Step 2: Understand the Project's Test Setup
Detect test framework — read package.json:
| Framework | Signals |
|---|---|
| Jest | jest, @jest/globals, ts-jest, babel-jest in devDependencies |
| Vitest | vitest, @vitest/ui in devDependencies |
| Bun test | "test": "bun test" in scripts, no jest/vitest present |
| Mocha | mocha, @types/mocha in devDependencies |
| Node test runner | node --test in scripts |
If no framework found, ask: "No test framework found. Which should we use?" (options: Jest / Vitest / Bun test).
Detect conventions — Glob: **/*.spec.ts, **/*.test.ts, **/*.spec.js, **/*.test.js, **/__tests__/**/*.ts. From 3–5 matches note: location pattern (co-located / __tests__ / tests/), naming, import style (path aliases), describe/it structure, mock patterns.
Step 3: Analyze the Changed Code
For each changed unit, summarize what changed and why in one sentence.
Step 4: Assess Unit Testability
This skill writes unit tests only. For each changed unit, determine whether a unit test suite is possible.
If the changed unit can be tested in isolation (pure logic, injectable deps, mockable boundaries) — proceed to Step 5.
❌ UNIT TEST NOT POSSIBLE — explain and stop
If the changed unit cannot be covered by a unit test suite, output the following and stop:
## Unit Test Not Possible
**Changed unit:** `file.ts:functionName`
**Change:** [one-sentence summary]
**A unit test suite cannot be written for this change because:** [specific reason — e.g., migration with no isolated logic, direct DB/HTTP call with no injection point, framework lifecycle wiring with no substitutable boundary]
Do not suggest alternative test types. Simply state that a unit test is not possible and why.
If ALL changed units are ❌, stop here. Do not proceed.
Step 5: Design the Test
For each ✅ unit, design the test before writing it.
5a: Find or determine the test file path
Check if a test file already exists for the changed file. If so, add to it. If not, determine the correct path from the project's conventions and create a new file.
5b: Design the test suite
Write a suite of unit tests that meaningfully covers the use cases introduced by the change.
5c: Test quality rules
- Hardcoded expected values only. Never compute the expected value using the same logic as the production code. Pre-calculate it (e.g. via
node -eorbun -e) and use a literal in the assertion. - Use
it.eachfor parameterized tests. When multiple test cases share the same structure with different inputs/outputs, useit.eachinstead of copy-pasting test bodies.
Step 6: Choose Destination
Show a brief summary for each designed unit test:
## Unit Test to Write
**File:** `src/services/user.service.spec.ts` [NEW / EXISTING]
**Framework:** Jest
**Unit under test:** `UserService.validateEmail()`
**Fix covered:** "Ensures emails with + characters are accepted"
Then ask destination once (for all unit tests together):
AskUserQuestion: "Where should the unit test(s) be written?" with options:
| Option | Description |
|---|---|
| Locally (Recommended) | Write directly to the working tree on the current branch |
| New branch + PR | Create a new branch off the current branch, write there, push and open a PR targeting the current branch |
If New branch + PR: see Branch + PR Creation section below before proceeding to Step 7.
Step 7: Write the Test
Add the test to the existing test file or create a new one following project conventions.
Verify TDD: check that the test fails against the source (before the change), then passes after it. If the test passes on both sides, it does not cover the change — revise it.
If the test fails to pass after the change, fix it and re-run once. If still failing, report the error and leave the file as-is.
Verify zero TS/lint errors. After the test passes at runtime, check IDE diagnostics or run the TS compiler to confirm zero type errors. Fix all errors before declaring done.
Branch + PR Creation
1. Detect current branch:
CURRENT_BRANCH=$(git branch --show-current)
2. Derive test branch name from the fix source:
| Source | Branch name |
|---|---|
PR #42 |
test/pr-42 |
Branch fix/user-auth |
test/fix-user-auth |
Commit abc1234 |
test/abc1234 |
Current branch feature/foo |
test/feature-foo |
3. Create the branch and switch:
git checkout -b <test-branch-name>
4. Write all confirmed test files (proceed with Step 7 write logic).
5. Commit and push:
git add <test-file-1> [test-file-2 ...]
git commit -m "test: add unit tests for <fix summary>"
git push -u origin <test-branch-name>
Important: Do NOT add Co-Authored-By, Signed-off-by, or any AI/Claude attribution to commits.
6. Open PR targeting the original branch:
gh pr create \
--base "$CURRENT_BRANCH" \
--title "test: <fix summary>" \
--body "$(cat <<'EOF'
## Unit tests
Adds unit tests for: **<fix summary>**
Tests added:
- <list each test file and what it covers>
These tests would have failed before the change was applied.
EOF
)"
Output the PR URL when done.
7. Apply label:
gh label create "unit-tests" --description "Unit tests PR" --color "0075ca" --force
gh pr edit $NEW_PR_NUMBER --add-label "unit-tests"
Output Format
## Unit Test: [PR title / branch / commit summary]
**Change:** [one-sentence summary]
**Source:** [PR #N / branch name / commit SHA]
---
### `src/services/user.service.ts` — `validateEmail()`
**Assessment:** ✅ Unit testable — pure logic, no external dependencies
**Test covers:** Email + character acceptance
**Test file:** `src/services/user.service.spec.ts` [created / updated]
**Test result:** ✅ Passed
---
### `src/db/migrations/0042_add_user_index.ts` — migration
**Assessment:** ❌ Unit test not possible
**Reason:** Database migration — no isolated logic to test.
---
## Summary
| Unit | Assessment | Test file | Result |
|---|---|---|---|
| `UserService.validateEmail` | ✅ Written | `user.service.spec.ts` | ✅ Pass |
| `0042_add_user_index` | ❌ Not possible | — | N/A |
Important
- Unit tests only — if a unit test suite is not possible, state why and stop; do not suggest other test types