diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-06 21:59:40 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-06 21:59:40 -0500 |
| commit | 241e23457bb2619541cdad170580341529607e90 (patch) | |
| tree | 4c82a5c93c3ded86b0f48c2f9edb52cbfbfedf03 /languages/typescript | |
| parent | 01cc47f067da15b3c87efc61c7b4eb97eb79d7d4 (diff) | |
| download | rulesets-241e23457bb2619541cdad170580341529607e90.tar.gz rulesets-241e23457bb2619541cdad170580341529607e90.zip | |
feat(languages): add typescript bundle (Vitest-canonical)
Mirrors the python bundle's minimal shape: one language-specific file under claude/rules/. Vitest is canonical, with brief notes for Mocha+Chai and Angular Karma legacy idioms. Covers RTL query priorities, MSW for network mocking, it.each for parametrize, async patterns, and TS-specific discipline (no any in tests, prefer satisfies, etc.).
Diffstat (limited to 'languages/typescript')
| -rw-r--r-- | languages/typescript/claude/rules/typescript-testing.md | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/languages/typescript/claude/rules/typescript-testing.md b/languages/typescript/claude/rules/typescript-testing.md new file mode 100644 index 0000000..bd6933f --- /dev/null +++ b/languages/typescript/claude/rules/typescript-testing.md @@ -0,0 +1,214 @@ +# 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. + +## 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. |
