1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
|
# TypeScript Testing Rules
Applies to: `**/*.{ts,tsx}`
Implements the core principles from `testing.md`. All rules there apply here —
this file covers TypeScript-specific patterns.
## Framework: Vitest (canonical)
Use Vitest for new TypeScript code. It's native ESM, native TS, fastest watch
mode, and shares its config with Vite when the project already uses it.
For legacy code, the same principles apply with different idioms:
- **Jest** — same `describe`/`it`/`expect` API; substitute `jest.mock` for
`vi.mock`. Most patterns below port directly.
- **Mocha + Chai** (Node backends) — `describe`/`it` are the same; assertions
use `expect(x).to.equal(y)` instead of `expect(x).toBe(y)`. Sinon for spies.
- **Angular + Karma** (`ng test`) — different planet. Follow Angular's testing
guide; the Normal/Boundary/Error category discipline still applies.
Don't mix frameworks in one package. Don't introduce a second framework to
"try it out" — pick the one that fits and commit.
## Test Structure
Group tests in `describe` blocks that mirror the source module. Use
`beforeEach` for setup that every test in the block needs:
```ts
import { beforeEach, describe, expect, it } from "vitest";
import { Cart } from "./cart";
describe("Cart", () => {
let cart: Cart;
beforeEach(() => {
cart = new Cart({ userId: 42 });
});
it("normal: adding an in-stock item increases quantity", () => {
cart.add("SKU-1", 2);
expect(cart.itemCount("SKU-1")).toBe(2);
});
it("boundary: quantity 0 is a no-op, not an error", () => {
cart.add("SKU-1", 0);
expect(cart.itemCount("SKU-1")).toBe(0);
});
it("error: negative quantity throws", () => {
expect(() => cart.add("SKU-1", -1)).toThrow(/quantity must be non-negative/);
});
});
```
Co-locate test files with the source under test (`cart.ts` ↔ `cart.test.ts`)
unless the project layout dictates a separate `tests/` tree. Match the project's
existing convention.
## Fixtures via Factory Functions
TypeScript has no pytest-style fixture system. Use plain factory functions for
test data — they're typed, refactor-safe, and explicit:
```ts
const makeUser = (overrides: Partial<User> = {}): User => ({
id: "u-1",
email: "alice@example.com",
role: "member",
...overrides,
});
it("admin can delete posts", () => {
const admin = makeUser({ role: "admin" });
expect(canDeletePosts(admin)).toBe(true);
});
```
Avoid `faker`/`@faker-js/faker` unless the project genuinely needs random data.
Random fixtures hide off-by-one bugs and make test failures non-reproducible.
Seed deterministically when faker is unavoidable.
## Parametrize for Category Coverage
Use `it.each` to cover normal, boundary, and error cases concisely instead of
hand-writing near-duplicate tests:
```ts
it.each<[number, boolean]>([
[1, true], // Normal
[100, true], // Normal: bulk
[0, true], // Boundary: zero is a no-op
[-1, false], // Error: negative
])("add(SKU-1, %i) — valid=%s", (quantity, valid) => {
if (valid) {
cart.add("SKU-1", quantity);
} else {
expect(() => cart.add("SKU-1", quantity)).toThrow();
}
});
```
### Pairwise / Combinatorial for Parameter-Heavy Functions
When `it.each` would require listing dozens of combinations (feature flags ×
permissions × shipping × payment × etc.), switch to combinatorial coverage via
`/pairwise-tests`. The skill generates a minimal matrix covering every 2-way
parameter interaction — typically 80-99% fewer cases than exhaustive,
catching most combinatorial bugs.
Workflow: invoke `/pairwise-tests` → get a PICT model + generated test matrix
→ paste the matrix into an `it.each` block. See `testing.md` § Combinatorial
Coverage for the general rule and when to skip.
## Measuring Coverage — `make coverage-summary`
The bundle ships a coverage summary at `.claude/scripts/coverage-summary.js`
and a Makefile fragment (`coverage-makefile.txt`) with `coverage` and
`coverage-summary` targets. After the suite runs under c8 (or Vitest/Jest with
the json-summary reporter) and writes `coverage/coverage-summary.json`, `make
coverage-summary` prints a file-weighted project number and the source files no
test imported.
The number to watch is that missing-file count. A module no test imports never
appears in the Istanbul report, so a statement-weighted total skips it silently
and the suite looks healthier than it is. The summary counts every `.ts`/`.js`
under the source dir that's absent from the report as 0%, so an untested module
drags the project number down where you can see it. It doesn't reimplement the
per-file table — nyc/c8 already print that. Copy the fragment's targets into
your own Makefile to adopt it; the bundle never edits your Makefile.
## Mocking Guidelines
### Mock these (external boundaries):
- HTTP clients (`fetch`, `axios`, `ky`) — prefer MSW (see below) over
client-level mocks where possible.
- File / filesystem APIs (`fs/promises`)
- Time (`vi.useFakeTimers()` + `vi.setSystemTime(...)`)
- Browser globals not covered by jsdom (`navigator.geolocation`,
`IntersectionObserver`, `ResizeObserver`)
- Third-party SDKs (Stripe client, AWS SDK clients)
### Never mock these (internal domain):
- Your own service, hook, or utility functions
- Type-narrowing helpers (`isFoo`, `assertBar`) — those are the work
- Validation libraries (`zod`, `valibot`) — they're framework, not boundary
- React's render lifecycle, hooks, or context — use RTL to exercise the real thing
If a unit test needs heavy internal mocking, the production code needs
restructuring (see `testing.md` § *If Tests Are Hard to Write, Refactor the
Code*).
## Async Testing
Use native `async`/`await`. Don't wrap in promise chains, don't mix `done`
callbacks:
```ts
it("processOrder resolves with status 'processed'", async () => {
const result = await processOrder(sampleOrder);
expect(result.status).toBe("processed");
});
```
For React component tests, use RTL's `findBy*` (auto-waits) over
`waitFor(() => getBy*)` — `findBy*` is purpose-built for the "appears
eventually" case.
## React Testing Library
When testing React components:
- **Query priority**: `getByRole` > `getByLabelText` > `getByPlaceholderText`
> `getByText`. Reach for `getByTestId` only when the others genuinely don't
fit. Test like the user — don't query CSS classes or DOM structure.
- **`userEvent` over `fireEvent`**. `userEvent` simulates real interaction
(focus, keyboard, async). `fireEvent` is a low-level escape hatch.
- **One assertion per behavioral concern**. A test that asserts "button
renders" + "button submits the form" + "form clears on submit" is three
tests.
- **Don't snapshot DOM**. Snapshots rot fast and reviewers rubber-stamp them.
Assert specific attributes and text instead.
## Network Mocking: MSW
For request-level mocking in component and integration tests, use MSW (Mock
Service Worker). It intercepts at the network layer, so the app code is
exercised end-to-end up to the boundary:
```ts
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
const server = setupServer(
http.get("/api/orders/:id", ({ params }) =>
HttpResponse.json({ id: params.id, status: "shipped" })
)
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
```
Prefer MSW over mocking `fetch` or `axios` directly — request mocks survive
client-library swaps; `vi.mock("axios")` doesn't.
## TypeScript-Specific Discipline
- **No `any` in tests.** Tests are documentation; `any` lies about the shape.
Use `unknown` and narrow, or define the precise type.
- **Prefer `satisfies` over type assertions.** `as` says "trust me"; `satisfies`
says "verify this conforms" without widening the inferred type.
- **Don't disable strict checks for tests.** Same `tsconfig` as production.
If a test needs `// @ts-expect-error`, leave a comment explaining the
invariant being verified.
- **Assertion functions for invariants.** When narrowing via runtime check is
expressive, write `assertIsFoo(value)` (returns `asserts value is Foo`)
instead of casting in every test.
## Anti-Patterns (TypeScript-Specific)
- Casting test data with `as Whatever` to silence a type error — fix the
factory or the type.
- Using `jest.mock` patterns in a Vitest project (or vice versa) — pick one.
- Snapshot-testing JSX trees — brittle, low-signal.
- Testing `useState` / `useReducer` directly via `renderHook` when the same
behavior is reachable through the component's UI — render the component.
- `expect(x).toBeTruthy()` when you mean `expect(x).toBe(true)` — they are
different invariants and the looser one masks bugs.
|