petrkindlmann/qa-skillsMIT2 files

Unit Testing

>-

Specification
Skill ID
petrkindlmann/qa-skills/unit-testing
Publisher
petrkindlmann
Repository
qa-skills
Installs
0
Files
2
License
MIT
Synced
Sep 23, 2026
How to use it

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.

  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.

// 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.

DoubleWhat it doesWhen to use
StubReturns canned data, no verificationControl a dependency's return value
SpyWraps real impl, records callsVerify calls without changing behavior
MockReplaces impl + records callsControl return AND verify interaction
FakeSimplified 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

TypeMeasuresBlind spots
BranchEvery if/else path taken?Misses value combinations
LineEach line executed?Misses untested branches in one line
StatementEach statement executed?Similar to line
FunctionEach 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=80

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)

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"]
}

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.

Use this skill