Fast Vite-aware unit testing with mocks, timers, snapshots, coverage and watch mode.
Topics
27
Sections
8
Learning module
Vitest learning module
Vitest is a next-generation testing framework powered by Vite: it reuses your Vite config and plugins, supports ESM, TypeScript and JSX out of the box, and offers a Jest-compatible API.
Tests run through the app's own Vite config: aliases, plugins and ESM just work.
vi.mock is hoisted; use vi.hoisted for values its factory needs.
test.projects runs several environments and configurations in one run.
Browser Mode gives real-browser fidelity when jsdom is not enough.
A test toolchain that fights the build toolchain: With a traditional runner, a modern TypeScript and ESM project needs a second set of transforms (Babel or ts-jest), duplicated path aliases and module mappers, and workarounds for ESM-only packages, and the whole suite re-transforms on every run. Configuration drifts from the app's build and feedback is slow. Vitest runs tests through the same Vite pipeline as the app, so aliases, plugins and ESM simply work, and its module-graph-aware watch mode reruns only what changed.
Test foundations
Test and describe
Define isolated cases and group related setup with a readable test structure.
One test asserts one behaviour and reads as a sentence in the report.
describe groups related cases and scopes the hooks they share.
Skip and only mark intent in the file, which review can see and CI can reject.
Assertions
Assertions
Use toBe for identity and primitives, toEqual for structural values, and precise matchers for errors.
toBe compares by reference, so two equal objects fail it.
toEqual walks the structure and ignores undefined properties by design.
Assert a thrown error by message or type rather than catching it by hand.
expect assertions
Require an expected assertion count when testing callbacks or asynchronous branches.
Declaring an expected count catches a callback that never ran.
A test asserting inside a promise passes silently when the promise is not awaited.
Pair it with a rejection path so the catch block is proven to execute.
Snapshot serializers
Normalize complex output before snapshotting so diffs show meaningful changes.
A serializer strips ids, timestamps and paths that change on every run.
Without it a snapshot fails for reasons unrelated to the change under review.
Keep the snapshot small enough that a reviewer can read the diff.
Test doubles
vi.fn and spies
Record calls and observe or replace existing methods with controlled test doubles.
A mock function records every call, its arguments and what it returned.
A spy wraps a real method so behaviour continues while calls are observed.
Restore a spied method afterwards or the next file inherits the replacement.
vi.mock
Mock modules at the import boundary while keeping factories and hoisting behavior explicit.
The call is hoisted above the imports, so the factory cannot close over later variables.
The whole module is replaced, including the parts the test does not care about.
Import the actual module inside the factory when only one export should change.
vi.stubGlobal
Replace browser or runtime globals with controlled values and restore them after the test.
Stubbing replaces a global such as fetch or matchMedia for one test.
The original is restored on unstub, which config can do automatically.
A leaked global stub shows up as a failure in a file that never touched it.
Mocked modules
Mock imports at the module boundary while respecting Vitest's hoisting rules.
Mocking at the import boundary leaves the module under test untouched.
The mocked module must be imported after mocking for the replacement to apply.
A partial mock keeps the real exports and overrides only the named ones.
Mock factories
Create reusable fake implementations that keep test intent visible and deterministic.
A shared factory keeps a fake in one place instead of per file.
Give the fake the same shape as the real thing, so a signature change breaks it.
Return fresh state per call, or tests share mutation through the fake.
Async testing
Fake timers
Control timeout and interval-driven code without waiting in real time.
Timer control turns a five-second wait into an instant, deterministic test.
Advancing time runs the callbacks due in that window, in order.
Restore real timers afterwards or an unrelated async test hangs.
Testing promises
Await asynchronous assertions and control rejection paths without flaky timing.
Await the assertion itself; an un-awaited rejection passes and then warns.
Assert rejection with a matcher rather than a try block that may never run.
Never mix a done callback with an async function in the same test.
Concurrent tests
Use concurrent execution only when tests have no shared mutable state.
Concurrent cases in one file share the module scope and any global state.
A shared spy or a mutated fixture is what makes concurrency flaky.
Use the scoped expect so a failure reports against the right case.
Quality
Watch mode and coverage
Shorten feedback loops locally and use coverage to guide missing behavioral cases.
Watch mode reruns only the files affected by the last edit.
Coverage shows which branches never ran, not whether the assertions were useful.
Chase an uncovered branch to its missing case rather than to a higher number.
Coverage providers
Choose a coverage provider and enforce meaningful thresholds in CI.
The V8 provider is fast; the instrumented provider maps source more precisely.
Thresholds fail the run, which is what turns a report into a gate.
Exclude generated files, or their untested lines set the number for everyone.
Configuration
Environment and globals
Select Node or jsdom environments and provide only the globals a test needs.
Choose jsdom for component work and node for everything that never touches the DOM.
A node environment runs faster and catches accidental browser dependencies.
Per-file environment comments let one project hold both kinds of test.
Test projects
Separate unit, browser and integration configurations while sharing common setup.
Projects let unit, browser and integration suites keep separate configuration.
One command runs them all and reports each project separately.
Shared setup stays in one place instead of being copied per configuration.
Test hooks
beforeEach and afterEach
Run setup and teardown around every test in scope, innermost hooks last on the way in.
Fresh state per test is what makes a failure reproducible in isolation.
Inner before hooks run after outer ones, and after hooks unwind in reverse.
An after hook still runs when the test fails, so cleanup is not skipped.
beforeAll and afterAll
Share expensive setup across a file, accepting that state now crosses test boundaries.
Setup runs once per file, so an expensive server starts a single time.
Anything mutated in one test is visible to the next, by construction.
Reserve it for genuinely read-only or externally reset resources.
Hook execution order
Nested describe blocks run outer before hooks first and outer after hooks last.
Outer setup completes before inner setup, mirroring the describe nesting.
Teardown unwinds inward first, so a resource is released before its owner.
Knowing the order explains a fixture that is unexpectedly already gone.
Cleanup by return value
Return a function from a before hook and Vitest runs it as that hook's teardown.
Returning a function keeps setup and its teardown in one place.
The returned cleanup runs even when the test throws.
It removes the matching after hook that was easy to forget.
onTestFinished
Register cleanup from inside the running test, next to the thing it has to undo.
Cleanup registered inside the test sits next to what it undoes.
It runs whether the test passed or failed, in reverse registration order.
This suits a resource created halfway through a single case.
onTestFailed
Capture extra diagnostics only when the test actually failed.
The callback runs only on failure, so diagnostics cost nothing when green.
Dump the request, the rendered markup or the server log at that moment.
It makes a rare CI failure debuggable without rerunning the suite locally.
Setup files
Move cross-file setup into setupFiles so every project shares one arrangement.
Setup files run before every test file in the project.
Matcher extensions and global test doubles belong here, not repeated per file.
Keep them small; work done here is paid once per file, not once per run.
Global setup
Start and stop a real dependency once per run in globalSetup, outside the test environment.
Global setup runs once per run, outside the test environment.
Start a database or a container here and tear it down in the returned function.
It cannot register matchers, because it does not share the test runtime.
Hook timeouts
Give a slow hook its own timeout instead of raising the limit for every test.
A slow hook has its own limit, separate from the test timeout.
Raising the global timeout to fix one hook hides real hangs everywhere else.
A hook that needs minutes is usually global setup in the wrong place.
Mock restoration between tests
Prefer restoreMocks and clearMocks in config over remembering a hook in every file.
Configuring restoration applies it to every file with no per-file hook.
Clearing wipes recorded calls; restoring puts the original implementation back.
A forgotten restore is the classic cause of a test that passes only in isolation.
Ask follow-up questions on any topic in your dashboard.