unit-tests
Systematically add unit test coverage for existing source files in any project, tracked across sessions. Use ONLY when the user runs /unit-tests <scan|plan|write> — do NOT trigger on ordinary one-off
SKILL.md.claude/skills/unit-tests/SKILL.md
GitHubUnit Tests
State files
.claude/skills/unit-tests/files.json— source file inventory + project test config.claude/skills/unit-tests/plan.md— pending test cases and blockers
Outside of write mode, these two files are the ONLY files this skill may write. Write mode may additionally create or edit test files in the configured test location.
Constraints (all modes)
- Never modify production code. If a file is untestable as-is (hidden dependencies,
static calls, no seams), record a blocker in
plan.mdand move on. - No new dependencies without asking.
- Match existing test conventions: before writing anything, read 1–2 existing test files and mirror their naming, structure, and mocking style. If the project has no tests at all, ask the user which framework to use — do not pick one.
- The ONLY command this skill may run is the project's test command from
files.jsonconfig, scoped to the test file just written. No git, no dev servers, no other commands. - When editing an existing test file, append new tests only — never rewrite, rename, or delete tests that are already there.
Mode selection
Argument scan, plan, or write selects the mode.
If the user names a specific file alongside the command, run plan mode then
write mode for that file only — skip the queue order.
No argument and no named file: ask which mode to run — do not read any files first.
Mode: scan — build or update the file inventory
- Detect project config (first scan only), store under
"config"infiles.json. If the repo has several testable projects (e.g., backend and frontend), ask the user which to include and store one config block per root. For each root:- Test framework and location: look for an existing test project or directory
(
*Tests.csproj,__tests__/,*.test.tssiblings,tests/+ pytest config) and confirm the framework frompackage.json/.csproj/pyproject.toml. - Test command template with a placeholder, e.g.
dotnet test --filter "FullyQualifiedName~{TestClass}"ornpx vitest run {testFile}orpytest {testFile}. - Test file naming convention, taken from existing test files.
- If anything is ambiguous or absent, ask the user.
- Test framework and location: look for an existing test project or directory
(
- Glob source files under each root and classify each by test value:
high— pure logic: utils, helpers, domain/service classes with injectable depsmedium— controllers, handlers, hooks with mockable dependenciesskip— config, generated code, type-only files, thin glue, markup-only components. Read a sample file when unclear from the name. Recordskipfiles too — it stops later scans from re-classifying them, and plan mode never touches them.
- For each
high/mediumfile, record its existing test file (mapped by naming convention) ornull. - Read
files.json(missing →{}). Add new files, keep existing entries unchanged, delete entries whose file no longer exists. Write it, report changes.
Schema — paths relative to repo root:
{
"config": {
"JobTrackerApi": {
"framework": "xunit",
"testDir": "JobTrackerApi.Tests",
"testCommand": "dotnet test --filter \"FullyQualifiedName~{TestClass}\"",
"naming": "{ClassName}Tests.cs"
}
},
"files": {
"JobTrackerApi/Services/LocalStorageService.cs":
{ "value": "high", "testFile": null, "planned": null, "tested": null }
}
}
Mode: plan — decide test cases, write no tests
- Read
files.json. If missing or empty, run scan mode first. - Process files with
"planned": nullone at a time, highest value first — at most 5 files per run. For each: read the file (and its existing test file, if any — plan only cases not already covered), append its test cases toplan.md, set the file'splanneddate. - Checklist for choosing cases:
- Test public behavior only — never private implementation details.
- Cover happy path, boundary values, and error/exception paths.
- For each nondeterministic dependency (clock, network, filesystem, random, database) name the mock or seam to use.
- One behavior per case; name by the project's convention
(e.g.
Method_Scenario_ExpectedResult). - Do not test framework code (EF, React, the test framework itself).
- If the file cannot be tested without production changes, record a
BLOCKEDblock explaining why instead of test cases. - Report what was planned and what remains.
Entry format in plan.md — one ## block per source file:
## JobTrackerApi/Services/LocalStorageService.cs
- Mocks: filesystem via temp directory; IConfiguration stub for UploadsPath
- [ ] SaveAsync_ValidFile_ReturnsStorageKey
- [ ] SaveAsync_EmptyFile_Throws
- [ ] DeleteAsync_MissingKey_DoesNotThrow
Blocked entry format — BLOCKED goes in the heading so write mode can skip it:
## JobTrackerApi/Helpers/TimeHelper.cs — BLOCKED
- Calls DateTime.Now directly, no injectable clock; needs a production refactor first
Mode: write — one source file per pass
- Read
plan.mdwithlimit: 30— only enough to get the FIRST non-blocked block. - Read the source file and one existing test file for a similar target; mirror its structure and mocking style.
- Create the test file per the naming convention (or append to the existing test file if one is recorded) and implement the listed cases. If a case no longer matches the source (member renamed or removed), drop or adjust it and say so in the report.
- Run the test command from the config block for the file's root, scoped to
this test file. On failure, fix the TEST code only — after 3 failed attempts
on a case, mark it
FAILING:inplan.mdand move on. If a failure reveals a real bug in production code, report it, mark that case with aBUG:note, and leave the production code untouched. - When all cases pass: remove the block from
plan.md, set the file'stesteddate andtestFileinfiles.json. IfBUG:/FAILING:cases remain: keep only those cases in the block, add— BLOCKEDto its heading, and still recordtestFilefor the tests that passed. - Report cases written and test results, state how many blocks remain, then STOP and wait for approval before the next file.