unit-testing

Write effective unit tests with Jest, Vitest, or pytest. Covers the test-doubles taxonomy (stub/spy/mock/fake), Arrange-Act-Assert, coverage threshold configuration and CI gating, snapshot testing, fake timers, and mutation testing with Stryker/mutmut. Use when: "unit test," "Jest," "Vitest," "pytes

By petrkindlmann · 687 installs

npx skills add petrkindlmann/qa-skills --skill unit-testing

Source repository · Upstream listing

<objective Write unit tests that fail when the code is wrong and pass when it is right — nothing weaker. A test that mocks every collaborator stays green while the integration is broken; the doubles taxonomy below stops that. A coverageThreshold typo (or the plural coverageThresholds , which Jest silently ignores) lets 40% covered code ship on a green pipeline; the config and Verification sections below make the gate actually fire. This skill covers Jest, Vitest, and pytest: doubles, coverage gating, snapshots, fake timers, and mutation testing as a behavior check on top of coverage. </objective Discovery Questions Check .agents/qa project context.md first — if it exists, use it and skip anything answered there. 1. Framework: Jest, Vitest, or pytest? Check package.json or pyproject.toml . The runner decides config keys and mock APIs. 2. Coverage tooling: Already configured? Look for jest.config. , vitest.config. , .nycrc , [tool.coverage] . Determines whether you add the gate or just tune it. 3. Mocking strategy: Manual mocks, auto mocking, or dependency injection? Check for mocks / dirs or DI containers — this sets which doubles you reach for. 4. Conventions: Co location ( .test.ts next to source) or a tests / tests/ tree? Match what exists; don't introduce a third location. Core Principles 1. Test behavior, not implementation. Verify what code does, not how . Refactoring internals should not break tests. 2. Fast, isolated, deterministic. No network/disk/DB. No shared mutable state. No uncontrolled Date.now() or Math.random() — freeze them with fake timers and seeded values. 3. Arrange Act Assert. One clear shape per test. 4. One assertion concept per test. Multiple expect calls are fine when they verify the same concept. 5. Descriptive names. "should [behavior] when [condition]" , not "test calculateTotal" . Framework Specific Patterns The full setup/teardown, mocking, spying, timer, in source, and monorepo examples for each runner live in references/patterns.md . Below is what is current and what to reach for; copy the code from the reference. Jest Current is Jest 30.x (30.4.2, May 2026). Jest 30 added collect tests , jest.config.mts support, Temporal aware fake timers, and clearMocksOnScope . If the code under test uses the Temporal API or time zone logic, Jest 30's Temporal aware fake timers remove a class of brittle setup. Reach for: jest.mock() for module boundaries ( jest.requireActual for partial mocks), jest.spyOn() to wrap a real method, jest.Mocked<T for typed mocks, and jest.useFakeTimers() for time. See references/patterns.md § Jest. Vitest Same API as Jest, Vite native. Stable: Vitest 4.1.x (June 2026); 5.0.0 beta is out (beta.3, May 2026). Vitest 4 added coverage.changed (changed files only coverage), mockThrow / mockThrowOnce , and a stable browser mode. Vitest 5 beta removes the sequential option and requires Node 22 / Vite 6.4 — wait for stable before adopting. Mock with vi.mock / vi.spyOn ; the standout features are in source testing ( import.meta.vitest ) and browser mode for component rendering. See references/patterns.md § Vitest. pytest Use fixtures + conftest.py (with yield for teardown), @pytest.mark.parametrize for data driven cases, and monkeypatch for env/attr substitution. Prefer fixtures over setUp / tearDown methods — fixtures compose and isolate per test. See references/patterns.md § pytest. Bun / Deno bun test (Jest compatible, no extra config) and deno test (native TS, permission flags) are reasonable defaults when your runtime is already Bun or Deno. Prefer Vitest/Jest for Node projects with deeper plugin ecosystems. Mocking Taxonomy Pick the simplest double that does the job. Most of the time that is a stub. Double What it does When to use Stub Returns canned data, no verification Control a dependency's return value Spy Wraps real impl, records calls Verify calls without changing behavior Mock Replaces impl + records calls Control return AND verify interaction Fake Simplified working impl (in memory DB) Complex stateful dependencies Rule of thumb: prefer stubs over mocks; reserve fakes for stateful dependencies; never call a real external API in a unit test. Only mock the external boundary (network, filesystem, DB, time) — let fast, deterministic internal collaborators run for real, or you get a suite that is green while the integration is broken. The four doubles in code: references/patterns.md § Test doubles. Coverage Configuration Jest — the threshold key is coverageThreshold (singular). The plural coverageThresholds is not a Jest key : Jest ignores it silently, the gate never enforces, and CI stays green at 30% coverage. This is the single most common config bug. Vitest — set test.coverage.thresholds in vitest.config.ts with provider: "v8" (see references/patterns.md § Vitest for the full block). pytest: Coverage types and what to gate on Type Measures Blind spots Branch Every if/else path taken? Misses value combinations Line Each line executed? Misses untested branches in one line Statement Each statement executed? Similar to line Function Each function called? Nothing about correctness Priority: Branch Line Statement Function. Use 80% line as the baseline gate , not a vanity target, and weight branch coverage higher. Focus coverage on business logic, transformations, error paths, and edge cases; skip generated code, type definitions, barrel exports, trivial getters, and framework boilerplate. For interpreting which uncovered lines matter and doing gap analysis, that's coverage analysis , not this skill. CI gate Jest and Vitest exit non zero when thresholds fail — that exit code IS the gate. pytest needs the flag explicitly: Mutation Testing Coverage tells you what code ran . Mutation testing tells you whether the tests would catch a bug . It makes small source changes ( → = , true → false ) and reruns the suite against each mutant. If the suite still passes, the mutant survived — your tests executed that logic but did not assert on it. Stryker (JS/TS) Stryker's own defaults are { high: 80, low: 60, break: null } — break: null means no failing exit. Set break (e.g. 50) to make a low score fail CI. Run: npx stryker run . mutmut (Python) — mutmut 3.x mutmut 3 dropped the old CLI surface. Configure paths in a [mutmut] block, run, then review survivors in the TUI: Avoid: mutmut run paths to mutate=src/ , mutmut results , and mutmut show 42 — that was the mutmut <3 surface. The paths to mutate flag is gone (paths move to the [mutmut] config block) and results / show are replaced by browse / apply (mutmut 3.5.x, verified June 2026). Following the old commands errors out on a current install. Interpreting scores Score Meaning 90%+ Strong — catching most logic changes 70–89% Decent — review survivors in critical paths <70% Tests execute code but do not verify behavior Run mutation testing on critical business logic , not the whole codebase (it is slow). Ignore equivalent mutants — logically identical code where no test could ever tell the difference. Snapshot Testing Use for: UI component render output, serialized data structures, CLI formatting — output where exact structure matters and is tedious to assert field by field. Do not use for: frequently changing output (snapshot fatigue → rubber stamp reviews), large snapshots (unreviewable), implementation details (CSS classes, internal IDs), or as a substitute for a targeted assertion when one specific value is what matters. Prefer inline snapshots for small output (<20 lines) and property matchers ( expect.any(String) ) for dynamic fields like ids and timestamps. Always run CI with ci so an unknown snapshot fails instead of being silently written and committed. Code: references/patterns.md § Snapshot testing. Anti Patterns Testing private methods — Test through the public API. If a private method really needs its own tests, extract it to its own module with a public surface. Mocking everything — Only mock external boundaries (network, filesystem, DB, time). A suite where every collaborator is mocked passes while the wiring between them is broken. The plural coverageThresholds — Jest ignores it; the gate never fires; CI is green at any coverage. The key is coverageThreshold (singular). See Coverage above. Faking all timers blindly — jest.useFakeTimers() / vi.useFakeTimers() with no allowlist can deadlock code awaiting a real microtask. Fake only what the test needs ( doNotFake / toFake ). See references/patterns.md § Jest timers. Async test without await — a forgotten await makes the assertion never run and the test passes vacuously. Add expect.assertions(n) / expect.hasAssertions() to async tests so a missing assertion fails them. Snapshot overuse — Use expect(x).toBe("active") for a specific value; reserve snapshots for structured output you can't assert field by field. Non descriptive names — Replace "works" with "should return empty array when no items match the filter" . Shared mutable state — Initialize in beforeEach , not at module scope: Verification Prove the suite runs and the gate actually fails on under coverage — the exact thing the coverageThreshold typo silently disables. 1. Tests run and pass: npx jest (or vitest run , pytest q ) exits 0 . 2. The gate bites. Run coverage and confirm a non zero exit when below threshold: Temporarily set a threshold above current coverage (e.g. 99) and confirm the command fails. If it exits 0 , your threshold key is wrong (likely the plural coverageThresholds ). 3. Snapshots are safe in CI: the run uses ci , so an unknown snapshot fails rather than being written. git status shows no new .snap after a CI mode run. Done When Coverage thresholds configured in jest.config. (key coverageThreshold , singular), vitest.config. ( coverage.thresholds ), or pyproject.toml ( fail under ) AND verified to exit non zero below threshold (Verification step 2) Test files all live in the project's single chosen location (co located OR tests / tests/ ) — git ls files shows no ad hoc test paths External boundaries (HTTP, DB, time) are mocked and internal collaborators are not — grep finds no real network/DB clients constructed in test files No test reaches outside the process boundary — suite passes with the network disabled and no test DB running CI runs the test command with ci (Jest/Vitest) so an unknown snapshot fails the build instead of being auto written Reference Files (in references/ ) patterns.md — full runnable examples per framework: Jest setup/teardown, module/spy/timer mocks, async guards; Vitest config, in source tests, concurrency, browser mode; pytest fixtures/parametrize/monkeypatch; Bun/Deno; the four test doubles; snapshot file/inline/property matchers. Related Skills coverage analysis — interpreting coverage reports, finding meaningful gaps, mutation score as a first class signal. Go there to read coverage; stay here to configure and gate it. ci cd integration — test stages in pipelines, parallelization, caching, deployment gating. ai test generation — when an AI writes the test code from a spec/PRD; this skill is for writing and structuring tests by hand. ai qa review — auditing existing tests for hallucinated APIs, fabricated imports, and closed loop tests. shift left testing — pre commit hooks, IDE integration, and TDD workflow around these tests.