testing-unit
Pattern of rule testing. Load when creating or editing *.spec.ts under Vitest — the ready-made layout of describe and it, the fixture builder, the scenario id in the title, a Connect procedure test wi
Files1
View on GitHubSKILL.md.claude/skills/testing-unit/SKILL.md
GitHub<!-- rt-kit v0.35.0 · patterns/testing-unit.md · 1d97fe499563 · правится надстройкой, не здесь -->
One
A fixture is built by a function with
A test on a pure function and on a procedure
Pattern of the rule testing. What must be true — the law
docs/constitution/verifiability.md.
When to use
- A
*.spec.tsnext to the source is being created or edited. - Logic has to be moved out of a component or a service so that there is something to check it with.
- A test on a Connect procedure is being written.
Imports are explicit
globals: true stands in the config, but the list is written anyway:
import { describe, expect, it } from 'vitest';
One describe per function
The block name matches the function name word for word; the it titles are sentences in Russian,
present tense, about behaviour, not about the internals:
describe('applyDayClick', () => {
it('SC-BK-19 — клик по занятому дню ничего не меняет', () => {
expect(applyDayClick(day('2026-08-12'), selection)).toEqual(selection);
});
});
The scenario id from docs/specs/<domain>/scenarios.md stands at the start of the title, followed
by a dash. Edge cases are separate it blocks in the same describe, not one test with a dozen
assertions.
A fixture is built by a function with Partial<T>
Not by a repeated literal:
function day(iso: string, overrides: Partial<ICalendarDay> = {}): ICalendarDay {
return {
iso,
dayOfMonth: Number(iso.slice(8)),
priceThb: 6000,
busyNight: isNightBusy(iso, BUSY),
past: false,
...overrides,
};
}
Shared constants (PRICING, BUSY) lie at the top of the file, at module scope.
A decision is moved into a pure function
The dominant form in this tree: the logic moves into *.logic.ts, *.util.ts or
*.calculator.ts and is checked by a call — without TestBed, without substituting dependencies.
Samples — libs/site/common/booking/util/src/lib/availability-calendar.logic.ts and
libs/api/<quote domain>/util/src/lib/quote.calculator.ts.
A procedure is called directly
The handler is the handle method of the procedure class in the feature layer of its domain. The
database double is written by hand; the sample is FakePrismaClient in
libs/api/<booking domain>/feature/src/lib/link-booking.procedure.spec.ts:
const prisma: FakePrismaClient = new FakePrismaClient();
await procedureWith(prisma, emitter).handle(request());
What is checked is the order of actions, the rollback when half of it is refused, the idempotence
of a repeat and what exactly went into the database. The layout of fields is guarded by the tests
of the api layer and is not repeated here.
A one-off proof test
A defect that is otherwise confirmed only by reading the code is proved by a test written for the time of the investigation: it raises the real procedure, substitutes its only way out and counts the calls.
globalThis.fetch = (): Promise<Response> => Promise.resolve(Response.json({ success: false }));
The number before the edit and the number after are the only form of answer such a check gives. "A hundred calls out of a hundred" and "twenty out of a hundred" can be told apart, and "the code looks right" cannot.
The second way to take the "before" is to put back the version of the file from the point where the branch diverged and run the branch's test: the test failing is the reproduction of the defect. After that the file is restored from the copy, and the restoration is checked by running the same test, not by memory.
Doubles of the neighbours are not invented: the working set is taken from the test of a neighbouring procedure of the same domain. A double assembled by type fails not on the assertion but on the call of a method it does not have.
Such a file is not committed. It lives until the end of the investigation and is removed
together with it; a check worth keeping is rewritten as an ordinary *.spec.ts with the scenario
id in the title and goes in the branch. The sign of a temporary file is that its title names no
scenario at all.
Green that checks nothing
A probe that expects silence goes paired with a probe that expects a refusal. Silence also comes when there was nothing to check with. A prefix before a command in the pipeline acts on one link, and a hook that got an empty value is silent exactly like a hook that has nothing to refuse. Two probes out of three are green then — they expected silence — and the miss is visible only by the third, which expects a refusal.
A probe on a mine that does not go off is green without the fix. A scenario on the table of refusal codes for a spec without procedures would have passed before the edit too: the audit never has to read that table at all. Such a probe is checked from both sides — the previous state is brought back for a minute and the probe is run: it did not turn red, so the probe guards nothing.
Common misses
- A lib without its own
vitest.config.mts:nx test <project>passes green without running a single file. Before writing the first test in a lib, check that the config is next to it. - The compiler import goes into the harness file, not into the spec. A lib that renders nothing
still pulls the framework when its test imports a barrel with a framework service, and the first
import fails on JIT. The import goes into the file named in
setupFilesof the lib's run config: in the spec the linter moves it below its neighbours, and the test turns red only in the full set. - Substituting a module (
vi.mock) would hide what the test is created for — which call went to the database and in what order. The double is written by hand. - Do not create your own helpers for assertions:
expect(...).toBe(...)and.toEqual(...)directly. - A title with a scenario id that does not exist takes
npm run check:specsdown, while the tests themselves stay green.