# 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 => ({ 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.