Zod uses Vitest (^4.1.5) as its only test runner. What makes the setup worth reading is that it tests the library's two products — runtime validation and static types — in the same files, and it runs everything from source.

The configuration

vitest.config.ts
export default defineConfig({
  resolve: {
    conditions: ["@zod/source", "default"],
    externalConditions: ["@zod/source", "default"],
  },
  // …
  test: {
    projects: ["packages/*"],
    watch: false,
    isolate: true,
    setupFiles: [resolve(__dirname, "scripts/fail-on-console.ts")],
    typecheck: {
      include: ["**/*.test.ts"],
      enabled: true,
      ignoreSourceErrors: false,
      checker: "tsc",
      tsconfig: "./tsconfig.json",
    },
    silent: true,

Three decisions to note:

  • conditions: ["@zod/source", ...] — tests import "zod" and "zod/v4" like a consumer, but resolution lands on packages/zod/src/*.ts. No build precedes pnpm test.
  • typecheck with checker: "tsc" — every *.test.ts file is compiled by real tsc, and expectTypeOf assertions are enforced. Type regressions fail the suite the same way runtime regressions do; assignability.test.ts and generics.test.ts exist purely for this. ignoreSourceErrors: false means library source errors fail tests too.
  • projects: ["packages/*"] — each workspace package is a Vitest project, so pnpm test also picks up packages/integration and friends.

The no-console guard

The single setup file turns any console output during tests into a failure:

scripts/fail-on-console.ts
function thrower(method: string) {
  return (...args: any[]) => {
    throw new Error(`Unexpected console.${method} call: ${args.join(" ")}`);
  };
}

beforeAll(() => {
  for (const method of ["log", "info", "warn", "error", "debug"] as const) {
    // @ts-ignore
    console[method] = thrower(method);
  }
});

This backs the repo rule of "no log statements in tests or production code" with enforcement rather than convention.

What the suite guarantees

The classic test directory alone (packages/zod/src/v4/classic/tests/) holds 80 files. Some named guarantees worth knowing about:

  • redos.test.ts reflects over every regex exported from core/regexes.ts (80+ materialized patterns) and runs recheck on each: "no built-in pattern is ReDoS-vulnerable" is a test, not a claim.
  • object.test.ts dedicates two describe blocks to __proto__: injected keys must not pollute prototypes in any of the jit/jitless/async paths, while a __proto__ key declared in the shape must round-trip as an own property.
  • detached-methods.test.ts pins the promise that const { parse } = schema keeps working — the reason the parse family is built as per-instance closures.
  • global-config.test.ts asserts z.config() state is shared through globalThis, so CJS and ESM copies of Zod loaded side by side see one config.
  • Inline snapshots (toMatchInlineSnapshot) are used heavily for error shapes and JSON Schema output, updated with pnpm vitest run --update.

Husky's pre-push hook runs the full pnpm test, so the suite is the effective merge gate. Commands for running subsets are in onboarding.

Sources: vitest.config.ts, vitest.root.mjs, scripts/fail-on-console.ts · last synced 2026-08-10 · 2d90846 · version 4.4.3