Testing & Quality
Testing & Quality

Vitest 4 in practice: config, environments, coverage, output

Lesson · ~30 min
After this unit you can
  • Read and write a Vitest 4 config — include, environment (and the per-file @vitest-environment comment), globals, setupFiles vs globalSetup — knowing each default
  • Explain what the default forks pool gives you over threads (process-level state such as process.env.TZ), and what turning isolate off lets leak between files
  • Turn on coverage with coverage.include set, and say what a coverage percentage does and doesn't prove
  • Judge when a snapshot test helps and when it rubber-stamps, and review a -u update like code
  • Get passing tests' console output back when a reporter hides it (the agent reporter, silent: "passed-only"), and predict what breaks when a suite moves to Vitest 5

Read

Simple
1 / 2

The config file, line by line

Flag

Vitest runs with no config at all. You add vitest.config.ts when a default doesn't fit — and you should know the defaults, because they're what you get for every line you leave out. This is Vitest 4.

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    // Which files are tests. Default: ["**/*.{test,spec}.?(c|m)[jt]s?(x)"]
    include: ["src/**/*.test.ts"],

    // Where tests run. Default: "node" — no DOM. "jsdom" or "happy-dom" for UI tests.
    environment: "node",

    // describe/it/expect as globals? Default: false — import them from "vitest".
    globals: false,

    // Files run before EACH test file, in the same worker as its tests.
    setupFiles: ["./test/setup.ts"],
  },
});

The one for this app is a good example of "only what's needed":

export default defineConfig({
  resolve: { alias: { "@": root } },   // the same @/ alias as tsconfig
  test: {
    environment: "node",               // lib/* logic is pure; no DOM needed
    include: ["lib/**/*.test.ts", "learning/**/*.test.ts"],
  },
});

include keeps Vitest from picking up test files it shouldn't — here, the challenge graders under content/, which have their own config and their own run.

Two commands cover daily use:

  • vitest — in an interactive terminal, starts watch mode: it re-runs the tests affected by each file you save.
  • vitest run — runs once and exits with a status code. That's what CI and package.json scripts use. Outside a terminal (CI, or a non-interactive shell) plain vitest runs once too.

Run part of the suite by passing a filter: vitest run cart runs files whose path contains cart; -t "starts empty" runs tests whose name matches.

Medium
1 / 2

Setup files, globals, and where your tests actually run

Flag

setupFiles vs globalSetup

test: {
  setupFiles: ["./test/setup.ts"],        // before EACH test file, inside its worker
  globalSetup: ["./test/global-setup.ts"], // ONCE, in the main process, before any worker starts
}
  • setupFiles run in the same process as the tests they precede, so anything they do to the test's world sticks: register matchers, install MSW's server.listen(), add an afterEach cleanup. Vitest re-runs them for every test file.
  • globalSetup runs once, before the workers exist, and can return a teardown. Use it for things that are expensive and shared — starting a test database or a server. It can't touch a test's globals, because it doesn't run where the tests run.

globals: true

With the default false, every test file imports describe, it, expect from "vitest". Turn it on and they're global, Jest-style — and add "vitest/globals" to compilerOptions.types in tsconfig.json so TypeScript knows. One practical consequence, from Vitest's docs: "some libraries, e.g., @testing-library/react, rely on globals being present to perform auto cleanup" — unmounting what each test rendered. With globals: false, call RTL's cleanup() in an afterEach in a setup file.

Pools: forks vs threads

Vitest runs test files in parallel workers. In Vitest 4 the default pool is "forks" — child processes. The alternative, "threads", uses worker threads in one process: often faster, but the docs say "you are unable to use process related APIs such as process.chdir()".

The same limit bites with time zones. A setup file that sets process.env.TZ = "Asia/Tokyo" works under forks and is silently ignored under threads — checked under Vitest 4.1: the forks run reports Asia/Tokyo, the threads run keeps the machine's zone. A suite that tests time-zone behaviour that way needs forks (and should assert the zone actually changed). The UTC vs local time unit covers time zones in tests in full.

isolate

By default (isolate: true) each test file gets a fresh copy of every module, so module-level state from one file can't leak into another. Turning it off is faster but brings that leak back — within a file, modules are always shared, which is why the testability unit moves state out of modules.

Complex

Where did my output go? Reporters, CI and agents

Flag

The same suite prints different things depending on where it runs, and Vitest decides some of it for you from the environment.

What changes in CI

Vitest reads the environment (through the std-env package) and, when it detects CI:

  • doesn't start watch mode — the default is !process.env.CI && process.stdin.isTTY;
  • rejects .only — allowOnly defaults to !process.env.CI, so a forgotten it.only fails the run instead of silently skipping the rest of the file;
  • adds the GitHub Actions reporter when GITHUB_ACTIONS is "true" and you haven't configured reporters, which turns failures into annotations on the PR.

What changes under an AI coding agent

Since Vitest 4.1, when Vitest detects it's being run by a coding agent (Claude Code, for instance) and no reporters option is set, it swaps the default reporter for the agent (minimal) reporter (4.0 has no such switch). Its docs: it "outputs a minimal report containing only failed tests and their error messages", and console logs from passing tests are suppressed.

Checked under Vitest 4.1.12: a test that runs console.log("HELLO") and passes prints nothing in an agent session; the same run with --reporter=default, or with the agent's environment variables unset, prints HELLO. Logs from failing tests still appear. So a debugging console.log that "never runs" may be running fine — the reporter is just dropping it. Ask for it back:

vitest run --reporter=verbose          # or --reporter=default

or pin reporters: ["default"] in the config so humans and agents see the same thing. The reverse switch exists too: silent: "passed-only" hides passing tests' logs on purpose, for any reporter.

The other reporters

default (summary, plus the tree for failures or a single file), verbose (every test as it finishes), tree, dot, and machine-readable ones like json and junit. Vitest 4 removed the basic reporter (use ["default", { summary: false }]).

When you upgrade to Vitest 5

Vitest 5 shipped in September 2026. Behaviour this track describes that changes (the first two checked by running the same file on 4.1.12 and 5.0.5):

  • an un-awaited .resolves/.rejects fails the test, even when the expectation is right (Vitest 4 auto-awaits it with a warning);
  • clearMocks defaults to true, so call history no longer carries over between tests — a test that counted a module-level mock's calls across tests goes red;
  • it needs Node 22.12+ and Vite 6.4+, and vite is no longer bundled: it becomes a required peer dependency (npm and pnpm install peers for you; Yarn users add it themselves);
  • the json and junit reporters write to files by default instead of stdout.

Read the migration guide before bumping; the full list is longer.

Quiz

In a project of mostly logic tests, one new component test fails with ReferenceError: document is not defined. What's the most targeted fix?

Flag

Where should MSW's server.listen() go so every test file's fetch calls are intercepted?

Flag

With globals: false, React Testing Library renders pile up within a test file — the second test finds two copies of a button. Why?

Flag

A setup file sets process.env.TZ = "Asia/Tokyo". After someone switches pool to "threads" for speed, date tests start seeing the machine's own time zone. Why?

Flag

Coverage says 92%, but you know two whole modules have no tests at all. What's going on?

Flag

A markup refactor fails 40 snapshot tests. The author runs vitest -u, and reviewers approve the PR without opening the .snap diffs. What has gone wrong?

Flag

An AI coding agent runs vitest run to debug. A console.log inside a passing test never shows up. What's the likeliest reason?

Flag

A file creates const send = vi.fn() at the top; its second test calls send() once and expects toHaveBeenCalledTimes(2), counting the first test's call. Green on Vitest 4, red after upgrading to Vitest 5. Why?

Flag

Fill in the blank

Give one file a simulated DOM, leaving the rest of the suite in Node.

Flag
// @vitest- happy-dom
import { render, screen } from "@testing-library/react";

Fill in the config keys.

Flag
export default defineConfig({
  test: {
    : ["./test/setup.ts"], // before each test file, in its worker
    : true,                // describe/it/expect without importing them
  },
});

Make untested files show up in the coverage report at 0% instead of being left out.

Flag
coverage: {
  : ["src/**/*.ts"],
},

Two CLI flags: see every test (and its logs) as it finishes, and accept intentional snapshot changes.

Flag
vitest run --reporter=
vitest run 

Predict the output

// test/setup.ts
process.env.TZ = "Asia/Tokyo";

// vitest.config.ts
test: { setupFiles: ["./test/setup.ts"], pool: "threads" }

// tz.test.ts — the machine's own zone is America/New_York
it("zone", () => {
  console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
});
Flag
// src/price.ts      — tested by src/price.test.ts
// src/untested.ts   — no test imports it

// vitest.config.ts — coverage on, no coverage.include

// $ vitest run --coverage
Flag
// registry.ts
export const seen: string[] = [];

// a.test.ts
it("a adds", () => { seen.push("a"); });

// b.test.ts — runs after a.test.ts, in the same worker
it("b expects empty", () => { expect(seen).toEqual([]); });

// vitest.config.ts
test: { isolate: false, maxWorkers: 1 }
Flag

Further reading