- Skill ID
- petrkindlmann/qa-skills/unit-testing
- Publisher
- petrkindlmann
- Repository
- qa-skills
- Installs
- 0
- Files
- 2
- License
- MIT
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
petrkindlmann/qa-skills/unit-testingInstalls these files- SKILL.md
- references/patterns.md
What this skill tells the agent
<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"]
}More from petrkindlmann
Use this skill in a project
Open any RiverX project, search the Skills panel for petrkindlmann/qa-skills/unit-testing, and install it in one click.
