bundles/testing/skills/testing-expert/SKILL.md
Framework-agnostic testing strategy — which level to test at, what coverage numbers mean, how to design a test that survives refactoring, how to choose test data, and how to kill flakes. Use when deciding what is worth testing, setting or defending a coverage target, reviewing the shape of an existing suite, or diagnosing a flaky or slow test. Framework-specific work routes to a specialist skill instead of being answered here.
npx skillsauth add shipshitdev/library testing-expertInstall 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.
The generalist front door for testing. It owns the decisions that hold no matter
which framework you are in: level, coverage, design, data, and
flakes. Framework mechanics — how to query a rendered component, how to build
a testing module, which HTTP harness to use — belong to the specialists listed
under Delegates To.
Inputs:
Outputs:
Creates/Modifies:
External Side Effects:
Confirmation Required:
Delegates To:
react-testing-library for anything touching a rendered React component or
hook: query selection, userEvent, async findBy/waitFor, provider
wrappers, renderHook, and RTL anti-patterns.nestjs-testing-expert for anything touching a NestJS module: testing modules,
provider and repository mocking, controller and service specs, and HTTP-level
end-to-end tests.test-runner to actually execute a suite and drive failures to green.tdd when the ask is to write the test before the implementation.playwright-e2e-init to scaffold browser end-to-end coverage from nothing.husky-test-coverage to enforce a coverage threshold at commit time.Read the question for framework signal first. Delegate on a hit; the specialist has depth this skill deliberately does not carry.
| Signal in the request | Route to |
|---|---|
| Rendered component, hook, screen, getByRole, userEvent, waitFor, RTL | react-testing-library |
| NestJS module, provider, controller, service spec, testing module, HTTP e2e | nestjs-testing-expert |
| "run the tests", "fix the failures" | test-runner |
| "write the test first", red-green-refactor | tdd |
| No framework signal — level, coverage, design, data, flakes | Answer here |
Completion bound: either a specialist is named, or the question is one of the five framework-agnostic concerns below.
Every behavior has one cheapest level that can actually prove it. Test there, once.
| Level | Proves | Cost | Reach for it when | |---|---|---|---| | Unit | A pure decision: branching, calculation, parsing, invariant | Milliseconds | The behavior is a function of its inputs | | Integration | Two or more real collaborators agree on a contract | Tens to hundreds of ms | The risk lives in the seam, not either side | | End-to-end | A user-visible journey works against the real wiring | Seconds | Failure would be silent and expensive everywhere else |
Shape follows from that rule rather than a quota: most suites settle near 70/20/10 unit/integration/e2e because most risk is decision logic. Treat a different shape as a signal to explain, not a defect to correct. A suite that is mostly end-to-end is slow and flaky; a suite that is only unit tests passes while the wiring is broken.
The duplicate-coverage test. Before writing a test, ask which existing test already fails if this behavior breaks. If one does, and it fails for a clear reason, the new test is redundant — spend the effort on an uncovered branch instead.
Push down, not up. A behavior tested at a level above the cheapest one is a slow test and a vague failure message. When an end-to-end test is the only thing covering a calculation, move the calculation's cases down to unit tests and leave one end-to-end test proving the journey is wired.
Coverage measures which lines ran, not whether anything was verified. A suite that executes every line and asserts nothing meaningful reports 100%.
Use these as review triggers, not as a gate to satisfy:
| Metric | Working target | What a miss actually tells you | |---|---|---| | Line | > 80% | Whole files or branches were never exercised | | Branch | > 75% | Error paths and edge conditions are untested — usually the real gap | | Function | > 85% | Dead code, or an entry point nobody tests | | Critical paths (auth, payment, data loss) | 100% | A failure here is unrecoverable; no exception |
Branch coverage is the honest number. Line coverage rises just by calling a function; branch coverage rises only when the failure and edge cases are actually exercised. When one number must be enforced, enforce branch.
Read the uncovered report, not the percentage. The question is always "is the uncovered code risky?" — untested error handling matters, an untested generated barrel file does not. Raise a threshold only after the gap it would catch is already closed, so the ratchet never blocks work it did not cause.
A test earns its keep by failing when behavior breaks and staying quiet when structure changes. Every rule below serves that one property.
Test behavior through the public surface. Assert on what a caller can observe: the return value, the persisted state, the emitted event, the rendered output. A test that reaches for a private field or asserts a call sequence breaks on every refactor and proves nothing about correctness.
One behavior per test. The test name states the behavior; the body proves exactly that. When a name needs "and", split it — a failure should point at one cause.
Name tests as claims. returns an empty list when the organization has no members reads as a specification in the failure output. test user service does
not.
Keep the three phases visible. Arrange the state, act once, assert the outcome. One act per test — a second action means a second test.
it('rejects a transfer that exceeds the available balance', async () => {
// Arrange
const account = makeAccount({ balance: 50 });
// Act
const result = await transfer(account, { amount: 75 });
// Assert
expect(result).toEqual({ ok: false, reason: 'insufficient_funds' });
});
Mock the boundary, keep the subject real. Replace what you do not own and cannot control — network, clock, filesystem, third-party service, randomness. Keep everything you are actually testing real. A test whose subject is mocked verifies the mock.
Prefer a fake to a mock at a seam. An in-memory implementation of a repository interface exercises real call sequences and survives refactoring; a per-test stub of every method encodes the current implementation and breaks when it changes.
Cover the error paths. Happy-path-only suites are where the branch-coverage gap hides. For each behavior, test the boundary value, the empty case, and the failure the caller must handle.
Delete tests that no longer earn their place. A test asserting removed behavior, or duplicating a cheaper test at a higher level, is maintenance cost with no signal. Remove it in the same change that made it redundant.
Data strategy decides whether a failing test explains itself.
Build with factories, override the relevant field. A factory supplies valid defaults; the test overrides only what it is about, so the significant value is the only thing on screen.
const user = makeUser({ role: 'admin' }); // role is the point of this test
Keep it minimal and realistic. Include the fields the behavior reads.
Realistic shapes catch validation and encoding bugs that 'foo' never will.
Give each test its own data. Shared fixtures mutated across tests create order dependence — the leading cause of flakes that reproduce only in CI. Create per test, or reset to a known state before each.
Keep data deterministic. A random value that fails once and passes on retry costs more than the coverage it bought. Reach for generated inputs only under an explicit property-based setup with a recorded seed.
A flaky test is a defect: it trains the team to rerun CI and to ignore red. Fix it or delete it — never retry it into silence. Diagnose by cause.
| Symptom | Cause | Fix |
|---|---|---|
| Passes alone, fails in the suite | Shared mutable state or order dependence | Isolate setup per test; reset state in beforeEach; run the suite in random order to prove it |
| Passes locally, fails in CI | Timing under a slower or parallel machine | Await the real condition rather than a fixed sleep; remove the arbitrary timeout |
| Fails at a date, hour, or timezone boundary | Real clock and ambient timezone | Freeze the clock; pin the timezone in test config |
| Intermittent assertion on a list | Unordered results asserted in order | Sort before asserting, or assert set membership |
| Fails under parallel workers | Shared external resource: database, port, temp path, cache key | Namespace the resource per worker |
| Fails right after an action | Asserting before the async effect settles | Wait for the observable outcome; never sleep a guessed duration |
Quarantine buys time; it does not fix. Marking a test skipped is acceptable for one change, tracked, with an owner. An untracked skip is deleted coverage that still looks green.
Completion bound: the test passes 20 consecutive runs in randomized order, including in CI's parallel configuration.
development
Coordinates a weekly engineering review of board accuracy, recent code changes, operational health, and scoped cleanup. Use for a recurring repository health review or a review of the last several days.
testing
Audits project board configuration and prepares explicitly requested setup, copy, or normalization changes while preserving the existing workflow and provider boundaries. Use when inspecting a board's fields, columns, scope, or configuration.
testing
Reconciles a project board with current work and delivery evidence, reports incomplete coverage and metadata gaps, and applies only approved provider-supported field changes. Use when auditing board drift, reviewing blocked work, or assessing upcoming delivery.
development
Walk through how a subsystem works. Use for "how does X work", code walkthroughs before changing something, and placement or ownership questions. Explains architecture, runtime flow, and onboarding mental models. Can critique architecture. Use why for motivation.