# TypeScript Testing — Derived Application > Applies Atelier's domain principles to TypeScript testing specifically. > Derives from `domains/` docs; introduces no new P-rules (D-063). > See `languages/typescript.md` for the language first-principles stub. ## Vitest and Jest (Testing P1 Tests as Specification, C2 Clarity) - **Vitest for new TS projects; Jest for legacy:** Vitest shares `vite`'s transform pipeline (no separate `ts-jest` config); Jest's ecosystem is broader. Either is acceptable — pick one per repo, do not mix. - **Tests co-located with source:** `user.ts` → `user.test.ts`. A test file far from its subject rots (Documentation P5 Discoverability). - **`describe`/`it` mirror the public API:** the test block names read as a specification ("User", "rejects an invalid email", "returns the persisted id"). A reader should understand the unit from test names alone (Testing P1). - **`expect` over `assert`:** Vitest/Jest matchers produce readable failure output (`expect(x).toBe(y)` → "expected 5, received 3"). Raw `assert` gives a stack trace and nothing else (Testing P6 Failure Specificity). ```typescript // user.test.ts import { describe, it, expect } from 'vitest'; import { createUser } from './user'; describe('createUser', () => { it('rejects an invalid email', async () => { await expect(createUser({ email: 'not-an-email' })).rejects.toThrow(ValidationError); }); it('returns the persisted id', async () => { const u = await createUser({ email: 'a@b.co' }); expect(u.id).toMatch(/^[a-z0-9]+$/); }); }); ``` ## Mock Discipline (Testing P2 Independence, Testing P7 Realism) - **Mock at the boundary, not the unit:** replace `fetch` or the DB client, not the function under test. Mocking the unit under test tests the mock, not the code (Testing P7 — realism). - **No partial mocks of the system under test:** if a method must be stubbed, the unit is too large. Extract a collaborator and mock that. - **Each test sets up and tears down its own state:** no shared mutable fixtures. A `beforeEach`/`afterEach` resets; a top-level `let` shared across tests is order-coupling (Testing P2 Independence). - **`vi.useFakeTimers()` for time-dependent code:** never call `Date.now()` directly in code under test; inject a `Clock` port. In tests, fake timers make `setTimeout` synchronous. ```typescript import { vi, beforeEach, afterEach } from 'vitest'; beforeEach(() => { vi.useFakeTimers(); global.fetch = vi.fn(); // boundary mock }); afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); }); ``` ## Type-Level Tests (Testing P1 Tests as Specification, Data P7 Type Fidelity) - **Type-level tests assert the type system, not runtime behavior:** `expectTypeOf().toMatchTypeOf` and `tsd`/`expect-type` fail the build when a type assertion is wrong. - **Negative type tests are required:** `// @ts-expect-error` proves the compiler rejects what it should. A `@ts-expect-error` that no longer errors is itself an error (the comment must be consumed). - **Branded types and utility types get type tests:** a `UserId` should not be assignable to `string`; a `Readonly` should not allow assignment. These invariants are part of the spec (Testing P1). - **Applies `data/P7` (type fidelity):** a type-level test is a regression test for the type checker — if a refactor silently widens a type, the test fails. ```typescript import { expectTypeOf } from 'expect-type'; import type { User, UserPatch, UserId } from './user'; test('UserPatch omits id and makes fields optional', () => { expectTypeOf().toMatchTypeOf<{ name?: string; email?: string }>(); expectTypeOf().not.toHaveProperty('id'); }); test('UserId is not assignable to bare string', () => { // @ts-expect-error — brand prevents widening const s: string = {} as UserId; expect(s).toBeDefined(); }); ``` ## Parametrize and Factories (Testing P3 Determinism, Testing P9 Edge Case Coverage) - **`it.each` / `test.each` for parametrized cases:** one table drives many runs; each row is an independent test with its own name and failure output. - **Factories over fixtures:** `makeUser(overrides)` returns a fresh object per call. A shared `const user = {...}` across tests couples them and breaks determinism when one test mutates it (Testing P3). - **Edge cases as rows, not special tests:** empty array, single element, max int, null, undefined — each a row in a `test.each` table. An ad-hoc `it('handles edge')` with multiple asserts hides which case failed (Testing P9 — edge case coverage, P6 failure specificity). - **Property-style tests via `fast-check`:** for invariants (e.g., "parse(serialize(x)) === x"), `fast-check` generates hundreds of inputs and shrinks failures to a minimal counterexample. ```typescript import { test, expect } from 'vitest'; import { makeUser } from './user.factory'; test.each([ { input: '', reason: 'empty' }, { input: 'a'.repeat(1000), reason: 'too long' }, { input: 'not-an-email', reason: 'no @' }, ])('rejects email: $reason', async ({ input }) => { await expect(makeUser({ email: input })).rejects.toThrow(ValidationError); }); ``` ## Determinism and Time (Testing P3 Determinism, Testing P9 Edge Case Coverage) - **No `Date.now()`, `Math.random()`, or `crypto.randomUUID()` in code under test:** inject a `Clock`, `Random`, and `IdGen` port. In tests, provide deterministic fakes. - **`--random` test order (Vitest `sequence.shuffle: true` default) catches order coupling:** a test that passes alone but fails in a suite has hidden state. The shuffle makes that state visible (Testing P2). - **Race-detector parallelism for async tests:** run async tests concurrently by default; a test that assumes serial execution breaks under parallelism. Vitest's `concurrent` flag surfaces the bug. ```typescript import { vi, test, expect } from 'vitest'; test.concurrent('parallel fetch does not interleave state', async () => { const store = new Store(); await Promise.all([store.put('a', 1), store.put('b', 2)]); expect(store.get('a')).toBe(1); expect(store.get('b')).toBe(2); }); ``` ## Cross-References - `domains/testing/pyramid.md` — where unit/type/integration tests sit; the type-level tests here are the base layer. - `domains/testing/fixtures.md` — factory-vs-fixture discipline applied via `makeUser`. - `domains/testing/first-principles.md` — Testing P1 Specification, P2 Independence, P3 Determinism, P9 Edge Coverage. - `languages/ts-types.md` — the branded types and utility types that type-level tests assert. - `languages/ts-async.md` — async tests use the cancellation/timeout patterns from that doc. - `languages/ts-tooling.md` — `ts-jest`/`vitest` config and the `expect-type`/`tsd` toolchain.