unit-testing
Skill2 starsChanged 3 months agoMITView on GitHub
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," "pytest," "mock," "coverage threshold," "test doubles," "mutation testing," "fake timers," "snapshot test." Not for: interpreting coverage reports or finding coverage gaps — use coverage-analysis; AI generating the test code for you — use ai-test-generation; auditing existing tests for smells — use ai-qa-review; browser/component rendering assertions — use cypress-automation or visual-testing. Related: coverage-analysis, ci-cd-integration, ai-test-generation, shift-left-testing.
- Installs packages
20 other projects use a version of this.
Adopted by
Most recently taken up by
- reddygopalkrishna/developer-agent-skillstheir own version, first seen Nov 8, 2026
- petrkindlmann/qa-skillsword for word, first seen Oct 25, 2026
- lggarrison/agent-skillstheir own version, first seen Oct 25, 2026
- r-portas/skillstheir own version, first seen Oct 20, 2026
- prinx/agentstheir own version, first seen Oct 20, 2026
- pulsejet/memoriestheir own version, first seen Oct 17, 2026
Used with
Projects with it also use database-testing, e2e-testing and 1 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
petrkindlmannSkilldatabase-testing
Validate database integrity, test migrations forward and backward, verify schema constraints, manage seed data, detect migration drift, and identify query performance issues. Covers PostgreSQL, MySQL, MongoDB with Prisma, TypeORM, Drizzle, and SQLAlchemy, plus Testcontainers test databases. Use when: "database test," "migration test," "migration rollback," "rollback test," "data integrity," "SQL test," "schema validation," "seed data," "query performance," "Testcontainers." Not for: synthetic data generation/masking at scale — use test-data-management; Docker/IaC test-environment provisioning — use test-environments; SQL injection — use security-testing. Related: test-data-management, test-environments, security-testing, ci-cd-integration.
Also in 3 projects that use this
etewiahSkille2e-testing
Playwright E2E testing and Lighthouse performance auditing. Use when setting up E2E tests, running Playwright tests, performing Lighthouse audits, or debugging E2E test failures.
Also in 4 projects that use this
Microsoft AzureSkillgit-workflow
Squad branching model: dev-first workflow with insiders preview channel
Also in 4 projects that use this
Other versions20 projects use a version of this.
Word for word · 1
- petrkindlmann/qa-skills★ 209100% same
Edited · 0
None found.
Wrote their own version · 19
- pulsejet/memories★ 3.9ksame name
- lichtblick-suite/lichtblick★ 1.2ksame name
- javierbrea/eslint-plugin-boundaries★ 1ksame name
- kid-sid/claude-spellbook★ 190same name
- reddygopalkrishna/developer-agent-skills★ 8same name
- dprakash2101/dotnet-production-agent-skills★ 4same name
- ffsshhttiikk/opencode-agents-skills★ 2same name
- axross/skills★ 1same name
- iamcaominhtien/kanban-board-mcp★ 1same name
- mejbaurbahar/software-tester-skills★ 1same name
9 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository27 other files it gives its agents.
- acquire-codebase-knowledgeSkill
- ai-qa-reviewSkill
- ci-cd-and-automationSkill
- code-review-and-qualitySkill
- code-simplificationSkill
- contract-testingSkill
- documentation-and-adrsSkill
- github-actions-hardeningSkill
- github-issuesSkill
- incremental-implementationSkill
- nodeSkill
- release-readinessSkill
- risk-based-testingSkill
- security-and-hardeningSkill
- skill-creatorSkill
- skill-optimizerSkill
- systematic-debuggingSkill
- test-strategySkill
- typescript-magicianSkill
- verification-before-completionSkill
- writing-skillsSkill
- narrative-continuitySkill
- narrative-developSkill
- narrative-evaluateSkill
- narrative-pack-createSkill
- narrative-reviseSkill
- narrative-writeSkill
The file
We split this skill into the rules it gives an agent
11 rules. 0 are common, 6 are distinctive, and 5 are very specific to this project.
0
Common
Most of the 16 edited versions we read keep these.
6
Distinctive
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 22
Write unit tests that fail when the code is wrong and pass when it is right — nothing weaker.
- Very specific: line 35
Check .agents/qa-project-context.md first — if it exists, use it and skip anything answered there.
- Distinctive: line 46
1. Test behavior, not implementation: Verify *what* code does, not *how*.
- Very specific: line 53
2. Fast, isolated, deterministic: No network/disk/DB. No shared mutable state.
- Very specific: line 104
Use fixtures + conftest.py (with yield for teardown), @pytest.mark.parametrize for data-driven cases, and monkeypatch for env/attr …
- Distinctive: line 128
Rule of thumb: prefer stubs over mocks; reserve fakes for stateful dependencies.
- Distinctive: line 255
Run mutation testing on critical business logic, not the whole codebase (it is slow).
- Very specific: line 269
Prefer inline snapshots for small output (<20 lines) and property matchers (expect.any(String)) for dynamic fields like ids and timestamps.
- Distinctive: line 320
The gate bites: Run coverage and confirm a non-zero exit when below threshold:
- Very specific: line 336
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
- Distinctive: line 338
No test reaches outside the process boundary — suite passes with the network disabled and no test DB running
“Common” is measured: kept by at least 6 in 10 of the 16 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
<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.
- Framework: Jest, Vitest, or pytest? Check
package.jsonorpyproject.toml. The runner decides config keys and mock APIs. - Coverage tooling: Already configured? Look for
jest.config.*,vitest.config.*,.nycrc,[tool.coverage]. Determines whether you add the gate or just tune it. - Mocking strategy: Manual mocks, auto-mocking, or dependency injection? Check for
__mocks__/dirs or DI containers — this sets which doubles you reach for. - Conventions: Co-location (
*.test.tsnext 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.
// Bad — implementation detail // Good — observable behavior
expect(svc._cache.size).toBe(3); expect(svc.getUser("abc")).toEqual({ id: "abc", name: "Alice" });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.
it("should apply discount for orders over $100", () => {
// Arrange
const order = createOrder({ subtotal: 150 });
const svc = new DiscountService(0.1);
// Act
const result = svc.apply(order);
// Assert
expect(result.total).toBe(135);
});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.
// jest.config.js
module.exports = {
coverageProvider: "v8",
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.{d,test,stories}.ts", "!src/**/index.ts"],
coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } },
};Vitest — set test.coverage.thresholds in vitest.config.ts with provider: "v8"
(see references/patterns.md § Vitest for the full block).
pytest:
# pyproject.toml
[tool.coverage.run]
source = ["src"]
omit = ["src/**/test_*.py", "src/**/conftest.py"]
[tool.coverage.report]
fail_under = 80
show_missing = true
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:"]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:
- run: pytest --cov=src --cov-fail-under=80Mutation 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)
npm i -D @stryker-mutator/core @stryker-mutator/jest-runner # or vitest-runner// stryker.config.json (Stryker's documented default; .mjs/.mts also load)
{
"testRunner": "jest",
"coverageAnalysis": "perTest",
"mutate": ["src/**/*.ts", "!src/**/*.test.ts"],
"thresholds": { "high": 80, "low": 60, "break": 50 },
"reporters": ["html", "clear-text", "progress"]
}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:
# setup.cfg (or a [tool.mutmut] table in pyproject.toml)
[mutmut]
paths_to_mutate=src/pip install mutmut # 3.5.x
mutmut run # paths come from config, not a flag
mutmut browse # interactive TUI: inspect and retest survivors
mutmut apply <mutant_id> # write a survivor to disk to see what it changedAvoid:
mutmut run --paths-to-mutate=src/,mutmut results, andmutmut show 42— that was the mutmut <3 surface. The--paths-to-mutateflag is gone (paths move to the[mutmut]config block) andresults/showare replaced bybrowse/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:
// Bad: shared mutation // Good: fresh per test
const items = []; let items: string[];
it("A", () => items.push("a")); beforeEach(() => { items = []; });
it("B", () => { it("A", () => { items.push("a"); expect(items).toHaveLength(1); });
items.push("b"); it("B", () => { items.push("b"); expect(items).toHaveLength(1); });
expect(items).toHaveLength(1); // FAILS
});Verification
Prove the suite runs and the gate actually fails on under-coverage — the exact thing the
coverageThreshold typo silently disables.
- Tests run and pass:
npx jest(orvitest run,pytest -q) exits0. - 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 exitsnpx jest --coverage --ci # Jest/Vitest exit !=0 below coverageThreshold vitest run --coverage # same for Vitest pytest --cov=src --cov-fail-under=80 # pytest exits !=0 below the floor0, your threshold key is wrong (likely the pluralcoverageThresholds). - Snapshots are safe in CI: the run uses
--ci, so an unknown snapshot fails rather than being written.git statusshows no new*.snapafter a CI-mode run.
Done When
- Coverage thresholds configured in
jest.config.*(keycoverageThreshold, singular),vitest.config.*(coverage.thresholds), orpyproject.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-filesshows no ad-hoc test paths - External boundaries (HTTP, DB, time) are mocked and internal collaborators are not —
grepfinds 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.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

