skills/writing-unit-tests/SKILL.md
Writes and reviews focused, deterministic unit tests that verify behavior through public interfaces instead of implementation details. Use whenever tests are added to code that already exists, and when the request mentions unit tests, test coverage, edge cases, mocks or test doubles, "add tests for this function", "cover this module", "these tests are flaky", or "this test breaks every refactor". Also use when reviewing an existing suite for brittleness, weak assertions, or missing boundary cases.
npx skillsauth add jaktestowac/awesome-copilot-for-testers writing-unit-testsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when the goal is a small, fast test that proves one behavior of one unit. It helps produce tests that read like a specification and survive refactors, instead of tests that mirror the code and break whenever it moves.
This skill is framework-agnostic. It does not assume a runner, an assertion library, or a mocking tool. Detect what the project already uses before writing anything; if nothing exists yet, ask which runner to target and describe the tests in neutral terms until you know.
Trigger phrases: "unit tests", "add tests for this", "cover this function", "test coverage", "edge cases", "mock this", "these tests are flaky", "this test breaks on every refactor", "are these tests any good?".
Typical situations:
Reach for this skill when the code already exists. If the tests should come first and drive the implementation, use test-driven-development instead - then return here for the standard each test it produces must meet.
A unit test exercises one unit of behavior in isolation, with no real I/O.
| The behavior lives in | Test level | Why | | ------------------------------------------ | ------------------------------------ | ----------------------------------------------- | | A calculation, rule, or transformation | Unit | Fast, exhaustive on edge cases, no setup cost | | Collaboration between two owned modules | Unit at the outer module's interface | Keeps the seam public without mocking internals | | A query, schema, or serialization contract | Integration | Only the real dependency can prove it | | The shape of an external provider's responses | Contract test | Stubs encode an assumption; a contract test is what notices when it rots | | A user-visible workflow across layers | End-to-end / functional | Unit tests cannot observe it |
Push edge cases down to the unit level and keep the higher levels thin. If a behavior can only be proven by crossing a process or network boundary, say so and route it to integration coverage rather than forcing it into a unit test with a wall of mocks.
Skip these unless they carry real logic:
A test with nothing to prove still costs review time and still breaks on refactors.
for loop hides which case failed.skip, an only, or a commented-out test carries an issue link and an owner, or it does not land.Before writing anything, state:
If the interface is unclear or the unit does too many things, say so. Untestable code is a design finding, not a testing failure.
The interface is the test surface: callers and tests cross the same seam. Wanting to test past the interface - reaching into internals, stubbing things the unit owns, asserting on private state - means the unit is the wrong shape, and that is worth reporting even when the tests get written anyway.
Inspect the repository before choosing a style:
Match the existing conventions. Do not introduce a new runner, library, or folder layout unless the user asks for it.
List the behaviors to cover before writing code. Each lens maps to a standard test design technique:
| Lens | Technique | What to cover | | ------------------- | ------------------------ | ------------------------------------------------------------------------------------ | | Happy path | - | The main expected behavior with typical input | | Input classes | Equivalence partitioning | One representative per meaningfully different input group | | Boundaries | Boundary value analysis | Empty, zero, one, min, max, off-by-one, just outside the range | | Rule combinations | Decision table | Each meaningful combination of the flags or conditions that drive branching | | Invalid input | Negative testing | Wrong type, malformed value, missing required field | | Error paths | Negative testing | Thrown errors, rejected results, failure return values | | State transitions | State transition testing | Behavior that depends on prior state or call sequence, including invalid transitions | | Contract guarantees | - | Idempotence, immutability of inputs, ordering, defaults | | Known-risky spots | Error guessing | Places this unit or its neighbors have broken before |
Prefer a representative case per class over exhaustive permutations. Coverage of behavior beats coverage of lines. When several inputs combine, cover the pairs that matter rather than the full cross-product.
Some behaviors are defined by a rule that holds across the whole input space, not by a handful of cases. Where the project already has a property-based library, a single property earns more than twenty examples:
decode(encode(x)) equals x for any xTwo conditions before reaching for one: the property must come from an independent source of truth, not from re-describing the implementation, and failures must be reproducible - record the seed and pin the failing case as a normal example test once it is found. Do not introduce a new library for this without asking.
Every test follows Arrange - Act - Assert, in that order and visibly separated:
Naming rules:
returns zero when the cart is emptyAssertion rules:
See ./resources/good-and-bad-tests.md for worked before/after pairs of each rule.
A test added to code that already works can fail for two very different reasons: you found a bug, or you wrote the wrong expectation. Decide which before changing anything.
A unit test must produce the same result on every machine, in any order, forever. Handle each source explicitly:
| Source | Handling |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Current time and dates | Inject a clock or freeze time; never assert against "now" |
| Randomness and generated IDs | Inject the generator or seed it |
| Timeouts, delays, retries | Use the runner's fake timers; never sleep for a real duration |
| Async results | Await or return every promise; assert on rejections explicitly, never with a bare try/catch that can silently pass |
| Concurrency and ordering | Assert on the set of outcomes when order is not part of the contract |
| Locale, timezone, encoding | Pin them, or assert on structured values instead of formatted strings |
| Environment variables, config | Set them per test and restore them afterwards |
| Shared fixtures | Rebuild state per test; never depend on data another test created |
If a test cannot be made deterministic at the unit level, that is the signal it belongs in integration coverage.
expect(items).toHaveLength(7) says nothing about why seven is rightSee ./resources/test-doubles-guide.md for the double taxonomy, the boundary rule, and how to design code that does not need heavy mocking.
When the code has no tests and its behavior is not documented, do not start from what it should do:
Say clearly which tests are characterization tests, so nobody mistakes them for a specification of intended behavior.
When the job is an existing test that fails intermittently, or a suite that has become too slow to trust:
skip is containment. Every retried test is an open defect reporting itself as a pass../resources/flaky-test-triage.md has the reproduction commands, the symptom-to-cause table, the retry and quarantine rules, and the slow-test cases.
When reviewing a whole suite rather than one test, add the suite-level signals: duplicated setup drifting across files, helpers that have grown into god objects, magic values with no stated meaning, skipped or commented-out tests with no issue link, global retries masking instability, committed secrets or personal data in fixtures, and a total runtime nobody owns.
Run the tests against ./resources/unit-test-review-checklist.md.
For a suite that already exists, or when the review is the whole job rather than the last step of authoring, use unslop-tests instead. It carries the named-tell list, the severity tiers, and the evidence ladder for proving a weak test really is weak.
Two checks matter most, and both are run, not imagined:
Then report what was actually done:
skip, only, or commented-out test with no issue link, quietly removing coverage./resources/good-and-bad-tests.md - before/after examples of each rule, in neutral pseudocode./resources/test-doubles-guide.md - dummy, stub, spy, fake, and mock; where the boundary is; designing for testability./resources/flaky-test-triage.md - reproducing a flake, symptom-to-cause table, retry and quarantine rules, slow-test causes./resources/unit-test-review-checklist.md - final quality gate for new or reviewed unit testsunslop-tests - the review pass on tests that already exist: the named-tell detector and the mutation-check gatewriting-unit-tests-quick - the compact version, for routine everyday tests that do not need the full workflowtest-driven-development - when the tests should drive the implementation instead of following itdesigning-test-data - when the inputs and boundary values need deliberate design firstdesigning-functional-tests - when the behavior belongs in functional or end-to-end coverage insteadstatic-code-analysis-typescript - when the underlying code quality is the real problemThis skill is complete when:
testing
Tests the customization assets themselves - skills, prompts, custom agents, instructions - the way a product is tested: activation cases that check an asset fires when it should and stays quiet when it should not, output-contract cases, safety cases, collision cases between assets competing for the same trigger, a weighted rubric scored blind, and a baseline-versus-candidate gate before an edit ships. Use when a skill is edited and nobody knows whether behaviour changed, when two skills fight over the same request, when a description is being tuned for discoverability, when a collection has grown past manual spot-checking, or when the request mentions skill evals, prompt regression, or "does this skill actually work".
development
Shapes QA output for the person who has to act on it: result and blocker in the first two lines, one decision per report, findings ordered by what they cost, the long artifact in a file and the decisions in the message, and magnitude stated in units the reader can count. Use when a report is accurate but nobody acts on it, when a finding set is too long to read under time pressure, when the same findings must be retold for a developer, a release manager, and an on-call engineer, or when the request mentions "too long", "make this readable", "just tell me what to do", "so what", or "summarize this for stakeholders". Pairs with unslop-answers, which makes the same report honest.
testing
Verifies that the lines and branches a change actually touched are executed by tests, using LCOV or Cobertura diff coverage instead of whole-repo percentages, and escalates uncovered high-risk changes into a blocking finding. Use when a pull request needs a coverage gate that unrelated tests cannot satisfy, when total coverage looks healthy but the diff is untested, when wiring diff coverage into CI, or when someone claims a change is covered because the suite is green.
development
Cuts AI tells from test code: tests that pass without proving anything, tautological assertions, mock-only tests, hardcoded waits, coverage theater, vague names, swallowed errors, retries used as fixes. Use whenever test code is written, changed, or reviewed, including tests produced as a side effect of a feature task, and when the request mentions "review these tests", "are these tests any good", "this test always passes", "this suite is flaky", or "clean up these tests". Must always apply to test code.