Section 1 · Why and What to Test
Setting Up Vitest
16 min3 min readCode playground
Lesson at a glance
Add Vitest to a Vite project, write and run a first test file, and use watch mode and filters to keep the feedback loop under a second.
- 1Install and run
- 2A first test file
- 3Where the config lives
- 4Running a slice of the suite
- 5In the playground
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
The fastest way to stop writing tests is a slow, fiddly setup. If running one test takes 15 seconds and three config files, you will skip it “just this once” until there are no tests. Vitest exists largely to fix that: it plugs into the Vite setup your app already has, so a first test takes about two minutes.
Install and run
In a Vite project (React, Vue, vanilla, it doesn’t matter):
npm install --save-dev vitestAdd two scripts to package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}npm test starts watch mode: Vitest runs the suite, then waits. When you save a file, it reruns only the tests that import it, using Vite’s module graph. On a laptop that is usually well under a second, which is what makes testing feel like part of coding instead of a chore at the end. npm run test:run runs everything once and exits; that is the one for CI (Vitest also turns watch off automatically when the CI environment variable is set).
If you prefer a visual runner, install @vitest/ui and start vitest --ui: it opens a browser dashboard listing every file and test, with failures, console output and the module graph, and it reruns on save like the terminal does. The official Vitest extension for VS Code gives you the same run and debug buttons beside each test in the editor. Pick whichever keeps you in flow; the tests are identical.
A first test file
Splitwise-lite keeps money as integer cents (more on why in the next lesson), so it needs a function that turns 1234 into "$12.34":
export function formatMoney(cents) {
const sign = cents < 0 ? '-' : '';
const abs = Math.abs(cents);
const dollars = Math.floor(abs / 100);
const rest = String(abs % 100).padStart(2, '0');
return `${sign}${dollars}.${rest}`;
}Put the test next to it, named so Vitest finds it:
import { describe, test, expect } from 'vitest';
import { formatMoney } from './money.js';
describe('formatMoney', () => {
test('formats dollars and cents', () => {
expect(formatMoney(1234)).toBe('$12.34');
});
test('pads single-digit cents', () => {
expect(formatMoney(505)).toBe('$5.05');
});
});describe groups related tests (it shows up as formatMoney > pads single-digit cents in the report). test (alias it) declares one test. expect(value).toBe(expected) is the assertion: it throws when the values differ, exactly like the expectEqual you wrote in the previous lesson.
Where the config lives
Vitest reads your existing vite.config.js, so aliases and plugins work in tests for free. Test options go under a test key:
/// <reference types="vitest/config" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'node',
},
});The triple-slash line gives your editor the types for the test key. environment: 'node' is the default, written out here so you know the switch exists. Pure logic like money formatting runs in Node, which is fast. Tests that need document (you write those in section 3) use jsdom or happy-dom, installed separately and set either here or per file with a // @vitest-environment jsdom comment at the top.
You will also see globals: true in some projects. It makes test and expect available without importing them, Jest style. Explicit imports are the default and play better with editors and linters, so this course imports them.
Running a slice of the suite
As the suite grows you will want to run less of it:
npx vitest money # only files whose path contains "money"
npx vitest -t "pads" # only tests whose name matches "pads"
npx vitest run --reporter=verbose # one pass, every test name listedInside a file, test.only(...) runs that test alone and test.skip(...) parks one. Both are handy while debugging and dangerous if committed, because a stray .only silently turns off the rest of the file. Vitest guards against that in CI: when the CI environment variable is set, a test marked .only fails with “Unexpected .only modifier” instead of quietly skipping its neighbours.
In the playground
The exercises below run in a browser sandbox with a Vitest-compatible subset: describe, test, expect, vi and hooks are already in scope, and the code under test (the subject) is loaded for you, so there are no imports. When you check your work, your tests run against the real subject and must pass, then against a few deliberately broken versions and must fail on each.
The next lesson looks closely at what goes inside a test and how to read the output when one fails.
Key takeaways
- Vitest reuses your Vite config, so the same aliases, plugins and transforms apply to your tests with almost no setup.
- Files named
*.test.jsor*.spec.js(and their ts, jsx and tsx variants) are picked up automatically; put each one next to the code it tests. - Run
vitestwhile you work (watch mode reruns affected tests on save) andvitest runin CI for a single pass. - The default environment is Node; switch to
jsdomorhappy-domonly for tests that need a DOM.