diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbf015f19..9b2a40ff6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,6 +70,14 @@ Add a `.npmrc` file with following line next to the `package.json`: VITEST_MODULE_DIRECTORIES=/node_modules/,/packages/ ``` +## Using Unreleased Commits + +Each commit on the main branch and PRs with a `cr-tracked` label are published to [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new). You can install a specific commit with: + +```bash +npm i https://pkg.pr.new/vitest@{commit} +``` + ## Pull Request Guidelines - Checkout a topic branch from a base branch, e.g. `main`, and merge back against that branch. diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index d57f189de..96505b697 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -721,6 +721,57 @@ export default ({ mode }: { mode: string }) => { }, ], }, + { + text: 'Learn', + collapsed: false, + items: [ + { + text: 'Writing Tests', + link: '/guide/learn/writing-tests', + docFooterText: 'Writing Tests | Learn', + }, + { + text: 'Using Matchers', + link: '/guide/learn/matchers', + docFooterText: 'Using Matchers | Learn', + }, + { + text: 'Testing Async Code', + link: '/guide/learn/async', + docFooterText: 'Testing Async Code | Learn', + }, + { + text: 'Setup and Teardown', + link: '/guide/learn/setup-teardown', + docFooterText: 'Setup and Teardown | Learn', + }, + { + text: 'Mock Functions', + link: '/guide/learn/mock-functions', + docFooterText: 'Mock Functions | Learn', + }, + { + text: 'Snapshot Testing', + link: '/guide/learn/snapshots', + docFooterText: 'Snapshot Testing | Learn', + }, + { + text: 'Testing in Practice', + link: '/guide/learn/testing-in-practice', + docFooterText: 'Testing in Practice | Learn', + }, + { + text: 'Debugging Tests', + link: '/guide/learn/debugging-tests', + docFooterText: 'Debugging Tests | Learn', + }, + { + text: 'Writing Tests with AI', + link: '/guide/learn/writing-tests-with-ai', + docFooterText: 'Writing Tests with AI | Learn', + }, + ], + }, { text: 'Browser Mode', collapsed: false, @@ -795,35 +846,35 @@ export default ({ mode }: { mode: string }) => { collapsed: true, items: [ { - text: 'Mocking Dates', + text: 'Dates', link: '/guide/mocking/dates', }, { - text: 'Mocking Functions', + text: 'Functions', link: '/guide/mocking/functions', }, { - text: 'Mocking Globals', + text: 'Globals', link: '/guide/mocking/globals', }, { - text: 'Mocking Modules', + text: 'Modules', link: '/guide/mocking/modules', }, { - text: 'Mocking the File System', + text: 'File System', link: '/guide/mocking/file-system', }, { - text: 'Mocking Requests', + text: 'Requests', link: '/guide/mocking/requests', }, { - text: 'Mocking Timers', + text: 'Timers', link: '/guide/mocking/timers', }, { - text: 'Mocking Classes', + text: 'Classes', link: '/guide/mocking/classes', }, ], diff --git a/docs/config/index.md b/docs/config/index.md index 2922ea3f8..e71824760 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -80,4 +80,10 @@ export default defineConfig(configEnv => mergeConfig( Since Vitest uses Vite config, you can also use any configuration option from [Vite](https://vitejs.dev/config/). For example, `define` to define global variables, or `resolve.alias` to define aliases - these options should be defined on the top level, _not_ within a `test` property. +## Automatic Dependency Installation + +Vitest will prompt you to install certain dependencies if they are not already installed. You can disable this behavior by setting the `VITEST_SKIP_INSTALL_CHECKS=1` environment variable. + +## Config Options + Configuration options that are not supported inside a [project](/guide/projects) config have icon next to them. This means they can only be set in the root Vitest config. diff --git a/docs/guide/features.md b/docs/guide/features.md index f29cdf6df..31bc5373a 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -14,11 +14,15 @@ import FeaturesList from '../.vitepress/components/FeaturesList.vue'
Learn how to write your first test by Video +::: tip +This page is a high-level overview of Vitest's capabilities. If you're new to Vitest, we recommend reading the [Learn](/guide/learn/writing-tests) tutorial first for a hands-on introduction. +::: + ## Shared Config between Test, Dev and Build Vite's config, transformers, resolvers, and plugins. Use the same setup from your app to run the tests. -Learn more at [Configuring Vitest](/guide/#configuring-vitest). +Learn more at [Configuring Vitest](/config/). ## Watch Mode diff --git a/docs/guide/index.md b/docs/guide/index.md index 6d81e9b01..21a1df20f 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -1,5 +1,8 @@ --- title: Getting Started | Guide +next: + text: Writing Tests + link: /guide/learn/writing-tests --- # Getting Started @@ -92,136 +95,13 @@ Test Files 1 passed (1) If you are using Bun as your package manager, make sure to use `bun run test` command instead of `bun test`, otherwise Bun will run its own test runner. ::: -Learn more about the usage of Vitest, see the [API](/api/test) section. +Your first test is passing! Continue to [Writing Tests](/guide/learn/writing-tests) to learn about organizing tests, reading test output, and the core testing patterns you'll use every day. -## Configuring Vitest - -One of the main advantages of Vitest is its unified configuration with Vite. If present, `vitest` will read your root `vite.config.ts` to match with the plugins and setup as your Vite app. For example, your Vite [resolve.alias](https://vitejs.dev/config/shared-options.html#resolve-alias) and [plugins](https://vitejs.dev/guide/using-plugins.html) configuration will work out-of-the-box. If you want a different configuration during testing, you can: - -- Create `vitest.config.ts`, which will have the higher priority -- Pass `--config` option to CLI, e.g. `vitest --config ./path/to/vitest.config.ts` -- Use `process.env.VITEST` or `mode` property on `defineConfig` (will be set to `test` if not overridden) to conditionally apply different configuration in `vite.config.ts`. Note that like any other environment variable, `VITEST` is also exposed on `import.meta.env` in your tests - -Vitest supports the same extensions for your configuration file as Vite does: `.js`, `.mjs`, `.cjs`, `.ts`, `.cts`, `.mts`. Vitest does not support `.json` extension. - -If you are not using Vite as your build tool, you can configure Vitest using the `test` property in your config file: - -```ts [vitest.config.ts] -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - // ... - }, -}) -``` - -::: tip -Even if you do not use Vite yourself, Vitest relies heavily on it for its transformation pipeline. For that reason, you can also configure any property described in [Vite documentation](https://vitejs.dev/config/). -::: +To run tests once without watching for file changes, use `vitest run`. You can also pass additional flags like `--reporter` or `--coverage`. For a full list of CLI options, run `npx vitest --help` or see the [CLI guide](/guide/cli). -If you are already using Vite, add `test` property in your Vite config. You'll also need to add a reference to Vitest types using a [triple slash directive](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-) at the top of your config file. - -```ts [vite.config.ts] -/// -import { defineConfig } from 'vite' - -export default defineConfig({ - test: { - // ... - }, -}) -``` - -See the list of config options in the [Config Reference](../config/) - -::: warning -If you decide to have two separate config files for Vite and Vitest, make sure to define the same Vite options in your Vitest config file since it will override your Vite file, not extend it. You can also use `mergeConfig` method from `vite` or `vitest/config` entries to merge Vite config with Vitest config: - -:::code-group -```ts [vitest.config.mjs] -import { defineConfig, mergeConfig } from 'vitest/config' -import viteConfig from './vite.config.mjs' - -export default mergeConfig(viteConfig, defineConfig({ - test: { - // ... - }, -})) -``` - -```ts [vite.config.mjs] -import { defineConfig } from 'vite' -import Vue from '@vitejs/plugin-vue' - -export default defineConfig({ - plugins: [Vue()], -}) -``` - -However, we recommend using the same file for both Vite and Vitest, instead of creating two separate files. -::: - -## Projects Support - -Run different project configurations inside the same project with [Test Projects](/guide/projects). You can define a list of files and folders that define your projects in `vitest.config` file. - -```ts [vitest.config.ts] -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - projects: [ - // you can use a list of glob patterns to define your projects - // Vitest expects a list of config files - // or directories where there is a config file - 'packages/*', - 'tests/*/vitest.config.{e2e,unit}.ts', - // you can even run the same tests, - // but with different configs in the same "vitest" process - { - test: { - name: 'happy-dom', - root: './shared_tests', - environment: 'happy-dom', - setupFiles: ['./setup.happy-dom.ts'], - }, - }, - { - test: { - name: 'node', - root: './shared_tests', - environment: 'node', - setupFiles: ['./setup.node.ts'], - }, - }, - ], - }, -}) -``` - -## Command Line Interface - -In a project where Vitest is installed, you can use the `vitest` binary in your npm scripts, or run it directly with `npx vitest`. Here are the default npm scripts in a scaffolded Vitest project: - - -```json [package.json] -{ - "scripts": { - "test": "vitest", - "coverage": "vitest run --coverage" - } -} -``` - -To run tests once without watching for file changes, use `vitest run`. -You can specify additional CLI options like `--port` or `--https`. For a full list of CLI options, run `npx vitest --help` in your project. - -Learn more about the [Command Line Interface](/guide/cli) - -## Automatic Dependency Installation +## Configuring Vitest -Vitest will prompt you to install certain dependencies if they are not already installed. You can disable this behavior by setting the `VITEST_SKIP_INSTALL_CHECKS=1` environment variable. +Vitest reads your `vite.config.*` by default, so your existing Vite plugins and configuration work out-of-the-box. You can also create a dedicated `vitest.config.*` for test-specific settings. See the [Config Reference](/config/) for details. ## IDE Integrations @@ -242,7 +122,7 @@ Learn more about [IDE Integrations](/guide/ide) | `vue` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/vue) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/vue?initialPath=__vitest__/) | | `marko` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/marko) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/marko?initialPath=__vitest__/) | | `preact` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/preact) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/preact?initialPath=__vitest__/) | -| `qwik`| [Github](https://github.com/vitest-tests/browser-examples/tree/main/examples/qwik) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/qwik?initialPath=__vitest__/) | +| `qwik` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/qwik) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/qwik?initialPath=__vitest__/) | | `react` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/react) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/react?initialPath=__vitest__/) | | `solid` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/solid) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/solid?initialPath=__vitest__/) | | `svelte` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/svelte) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/svelte?initialPath=__vitest__/) | @@ -250,53 +130,6 @@ Learn more about [IDE Integrations](/guide/ide) | `typecheck` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/typecheck) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/typecheck?initialPath=__vitest__/) | | `projects` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/projects) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/projects?initialPath=__vitest__/) | -## Projects using Vitest - -- [unocss](https://github.com/unocss/unocss) -- [unplugin-auto-import](https://github.com/antfu/unplugin-auto-import) -- [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) -- [vue](https://github.com/vuejs/core) -- [vite](https://github.com/vitejs/vite) -- [vitesse](https://github.com/antfu/vitesse) -- [vitesse-lite](https://github.com/antfu/vitesse-lite) -- [fluent-vue](https://github.com/demivan/fluent-vue) -- [vueuse](https://github.com/vueuse/vueuse) -- [milkdown](https://github.com/Saul-Mirone/milkdown) -- [gridjs-svelte](https://github.com/iamyuu/gridjs-svelte) -- [spring-easing](https://github.com/okikio/spring-easing) -- [bytemd](https://github.com/bytedance/bytemd) -- [faker](https://github.com/faker-js/faker) -- [million](https://github.com/aidenybai/million) -- [Vitamin](https://github.com/wtchnm/Vitamin) -- [neodrag](https://github.com/PuruVJ/neodrag) -- [svelte-multiselect](https://github.com/janosh/svelte-multiselect) -- [iconify](https://github.com/iconify/iconify) -- [tdesign-vue-next](https://github.com/Tencent/tdesign-vue-next) -- [cz-git](https://github.com/Zhengqbbb/cz-git) - - - -## Using Unreleased Commits - -Each commit on main branch and a PR with a `cr-tracked` label are published to [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new). You can install it by `npm i https://pkg.pr.new/vitest@{commit}`. - -If you want to test your own modification locally, you can build and link it yourself ([pnpm](https://pnpm.io/) is required): - -```bash -git clone https://github.com/vitest-dev/vitest.git -cd vitest -pnpm install -cd packages/vitest -pnpm run build -pnpm link --global # you can use your preferred package manager for this step -``` - -Then go to the project where you are using Vitest and run `pnpm link --global vitest` (or the package manager that you used to link `vitest` globally). - ## Community If you have questions or need help, reach out to the community at [Discord](https://chat.vitest.dev) and [GitHub Discussions](https://github.com/vitest-dev/vitest/discussions). diff --git a/docs/guide/learn/async.md b/docs/guide/learn/async.md new file mode 100644 index 000000000..02d137dcc --- /dev/null +++ b/docs/guide/learn/async.md @@ -0,0 +1,136 @@ +--- +title: Testing Asynchronous Code | Guide +prev: + text: Using Matchers + link: /guide/learn/matchers +next: + text: Setup and Teardown + link: /guide/learn/setup-teardown +--- + +# Testing Asynchronous Code + +JavaScript code frequently runs asynchronously. Whether you're fetching data, reading files, or waiting on timers, Vitest needs to know when the code it is testing has completed before moving on to the next test. Here are the patterns you'll use most often. + +## Async/Await + +The most straightforward approach is to make your test function `async`. Vitest will automatically wait for the returned promise to resolve before considering the test complete. If the promise rejects, the test fails with the rejection reason. + +```js +import { expect, test } from 'vitest' + +function fetchUser(id) { + return Promise.resolve({ id, name: 'Alice' }) +} + +test('fetches user by id', async () => { + const user = await fetchUser(1) + expect(user.name).toBe('Alice') +}) +``` + +This is the pattern you'll use the vast majority of the time. It reads just like synchronous code, and errors propagate naturally through `await`. + +## Resolves and Rejects + +Sometimes you'd rather assert on a promise directly instead of `await`-ing it into a variable first. The [`.resolves`](/api/expect#resolves) and [`.rejects`](/api/expect#rejects) helpers let you do this. They unwrap the promise and then apply the matcher to the resolved or rejected value: + +```js +test('resolves to Alice', async () => { + await expect(fetchUser(1)).resolves.toMatchObject({ name: 'Alice' }) +}) + +test('rejects with an error', async () => { + await expect(fetchInvalidUser()).rejects.toThrow('User not found') +}) +``` + +::: warning +Don't forget the `await` before `expect`. Vitest will detect unawaited assertions and print a warning at the end of the test, but it's best to always include `await` explicitly. Vitest will also wait for all pending promises in `Promise.all` before starting the next test, but relying on this behavior makes tests harder to understand. +::: + +## Callbacks + +Some older APIs use callbacks instead of promises. Since Vitest works with promises, the simplest approach is to wrap the callback in a `Promise`: + +```js +function fetchData(callback) { + setTimeout(() => callback('peanut butter'), 100) +} + +test('the data is peanut butter', async () => { + const data = await new Promise((resolve) => { + fetchData(resolve) + }) + expect(data).toBe('peanut butter') +}) +``` + +This pattern works for any callback-based API. Pass `resolve` as the success callback, and the test will wait until the callback is invoked. + +## Timeouts + +By default, each test has a 5-second timeout. If a test takes longer than that (perhaps because a promise never resolves, or a network request hangs), it will fail with a timeout error. This prevents your test suite from getting stuck indefinitely. + +You can set a [custom timeout](/api/test#timeout) as the third argument to `test`, which is useful for tests that legitimately need more time: + +```js +test('long-running operation', async () => { + await someSlowOperation() +}, 10_000) // 10 seconds +``` + +If you find yourself needing longer timeouts across many tests, you can change the default for all tests with the [`testTimeout`](/config/testtimeout) config option: + +```js [vitest.config.js] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + testTimeout: 10_000, + }, +}) +``` + +## Concurrent Tests + +By default, tests within a file run one after another. This is usually what you want, especially when tests share setup code. But if you have many independent async tests that each spend most of their time waiting (on network, disk, timers, etc.), running them concurrently with [`test.concurrent`](/api/test#concurrent) can significantly speed things up: + +```js +test.concurrent('first async test', async () => { + const result = await fetchUser(1) + expect(result.name).toBe('Alice') +}) + +test.concurrent('second async test', async () => { + const result = await fetchUser(2) + expect(result.name).toBe('Bob') +}) +``` + +See the [Parallelism](/guide/parallelism) guide for the full picture of how Vitest runs tests in parallel, both across files and within them. + +## Unhandled Rejections + +By default, Vitest reports unhandled promise rejections as errors in the test run. If a promise rejects somewhere in your code and nothing catches it, the test run will fail, even if all your assertions passed. This is intentional: unhandled rejections usually indicate real bugs, like a forgotten `await` or a fire-and-forget promise that silently fails. + +```js +test('this causes an unhandled rejection error', () => { + // This promise rejects but is never awaited or caught + Promise.reject(new Error('oops')) +}) +``` + +To fix this, make sure you `await` all promises or catch expected rejections: + +```js +test('handle the rejection', async () => { + // Either await the promise + await expect(Promise.reject(new Error('oops'))).rejects.toThrow('oops') + + // Or catch it explicitly if you don't need to assert on it + Promise.reject(new Error('expected')).catch(() => {}) +}) +``` + +If your code intentionally produces unhandled rejections, you can filter specific errors with [`onUnhandledError`](/config/onunhandlederror) or disable the check entirely with [`dangerouslyIgnoreUnhandledErrors`](/config/dangerouslyignoreunhandlederrors). diff --git a/docs/guide/learn/debugging-tests.md b/docs/guide/learn/debugging-tests.md new file mode 100644 index 000000000..ccf66a310 --- /dev/null +++ b/docs/guide/learn/debugging-tests.md @@ -0,0 +1,208 @@ +--- +title: Debugging Failing Tests | Guide +prev: + text: Testing in Practice + link: /guide/learn/testing-in-practice +next: + text: Writing Tests with AI + link: /guide/learn/writing-tests-with-ai +--- + +# Debugging Failing Tests + +This page covers how to investigate test failures in Vitest: reading error output, isolating problems, identifying common causes, and using the available debugging tools. + +## Reading the Error + +When a test fails, Vitest gives you several pieces of information. Let's look at a real failure and break it down: + +<<< ./snippets/debug-output-fail.ansi + +There's a lot here, but each part tells you something: + +**The header** (`FAIL src/user.test.js > createUser > sets the default role`) tells you which file, describe block, and test failed. This is the full path in the test tree. + +**The assertion message** (`expected { ... } to deeply equal { ... }`) tells you what kind of check failed and shows the two values being compared. + +**The diff** shows exactly what's different. Lines starting with + are what you actually got, and lines starting with - are what you expected. In this case, the role was "viewer" but the test expected "member". + +**The code snippet** shows the exact line and a few surrounding lines, with a caret (`^`) pointing to the failing assertion. You can click the file path in most terminals and IDEs to jump directly there. + +At this point, the question is: did the code change (maybe the default role was intentionally updated to `"viewer"`), or is the test wrong? Check the source code for `createUser` to find out. If the default was intentionally changed, update the test. If not, you've found a bug. + +## Isolating the Problem + +When a test fails and the cause isn't immediately clear, the first step is to isolate it. Run just that one test, without the rest of your suite: + +```bash +# Run only the failing test file +vitest src/user.test.js + +# Run only tests matching a name pattern +vitest -t "sets the default role" + +# Combine both for maximum precision +vitest src/user.test.js -t "sets the default role" +``` + +You can also add [`.only`](/api/test#only) to the test itself: + +```js +test.only('sets the default role', () => { + // only this test runs in the file +}) +``` + +If the test passes when run alone but fails when run with others, you have a test isolation problem (more on that below). If it fails even when run alone, the issue is in the test itself or the code it's testing. + +## Common Causes of Failures + +### Shared State Between Tests + +This is one of the most common and frustrating issues. A test passes when you run it alone, but fails when the full suite runs. The usual cause is that some other test modifies shared state (a global variable, a module-level cache, a database) and doesn't clean up after itself. + +```js +// This is a problem: `users` is shared between tests +const users = [] + +test('adds a user', () => { + users.push('Alice') + expect(users).toEqual(['Alice']) +}) + +test('starts empty', () => { + // This fails because 'Alice' is still in the array! + expect(users).toEqual([]) +}) +``` + +The fix is to reset the state before each test with [`beforeEach`](/api/hooks#beforeeach), or better yet, use [`test.extend`](/guide/test-context#extend-test-context) to create fresh state for each test automatically: + +```js +const test = baseTest.extend('users', () => []) + +test('adds a user', ({ users }) => { + users.push('Alice') + expect(users).toEqual(['Alice']) +}) + +test('starts empty', ({ users }) => { + // Passes: each test gets its own array + expect(users).toEqual([]) +}) +``` + +### Async Issues + +Tests that involve promises can fail intermittently or in confusing ways if the async flow isn't handled correctly. The most common mistake is forgetting an `await`: + +```js +// This test always passes, even if fetchUser rejects! +test('fetches user', () => { + // Missing await: the test finishes before the promise settles + expect(fetchUser(1)).resolves.toMatchObject({ name: 'Alice' }) +}) +``` + +Vitest will usually warn you about unawaited assertions at the end of the test. If you see that warning, add the missing `await`: + +```js +test('fetches user', async () => { + await expect(fetchUser(1)).resolves.toMatchObject({ name: 'Alice' }) +}) +``` + +If a test hangs and eventually times out, it usually means a promise never resolves. Check for missing callbacks, unresolved conditions, or deadlocks in the code you're testing. + +### Stale Snapshots + +If you're using [snapshot tests](/guide/learn/snapshots) and you intentionally changed the output of your code, the existing snapshots will be outdated. The test fails and shows a diff between the old snapshot and the new output. + +This is expected. Review the diff to confirm the changes are correct, then update the snapshots by pressing `u` in watch mode or running `vitest -u`. + +### Wrong Test Environment + +If your code accesses browser APIs like `document` or `window` and you see errors like "document is not defined", your test is running in the Node environment (the default). You can switch to a browser-like environment with the [`environment`](/config/environment) config option, or better yet, use [Browser Mode](/guide/browser/) which runs tests in a real browser. + +### Mocks Not Cleaned Up + +If a mock from one test leaks into another, you'll get unexpected behavior. For example, a `vi.spyOn` that overrides a method's return value will persist into the next test unless it's restored. + +The easiest fix is to enable automatic mock restoration in your config: + +```js [vitest.config.js] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + restoreMocks: true, + }, +}) +``` + +This calls [`mockRestore()`](/api/mock#mockrestore) on every mock after each test. See the [Mock Functions](/guide/learn/mock-functions#resetting-mocks) tutorial for more details. + +## Debugging Tools + +### Console Logging + +There's nothing wrong with adding `console.log` to your tests. It's the fastest way to inspect values and understand what's happening: + +```js +test('transforms data correctly', () => { + const input = getData() + console.log('input:', input) + + const result = transform(input) + console.log('result:', result) + + expect(result).toMatchObject({ status: 'ok' }) +}) +``` + +Vitest displays console output inline with the test results, so you can see which test produced which log. + +### Vitest UI + +For a visual overview of your test suite, run Vitest with the `--ui` flag: + +```bash +vitest --ui +``` + +This opens a browser-based dashboard where you can see all your tests, their status, and their output. It also includes a module graph that shows how your files are connected, which can help you understand why a change in one file causes failures in another. See the [Vitest UI](/guide/ui) guide for more details. + +### VS Code Extension + +The [Vitest VS Code extension](https://vitest.dev/vscode) lets you run and debug individual tests directly from your editor. You can click a "play" button next to any test, set breakpoints, and step through code in the VS Code debugger. This is often faster than switching between the terminal and your editor. + +### Verbose Output + +If the default output isn't showing enough detail, use the verbose reporter: + +```bash +vitest --reporter=verbose +``` + +This shows every test individually (not just the files), which can help spot patterns in which tests pass and which fail. + +### Attaching a Debugger + +For more complex issues where you need to step through code line by line, you can attach a debugger. See the [Debugging](/guide/debugging) guide for setup instructions for VS Code, IntelliJ, and Chrome DevTools. + +## Getting Help + +If you're stuck, these resources can help: + +- The [Common Errors](/guide/common-errors) page covers specific error messages and their solutions +- [GitHub Issues](https://github.com/vitest-dev/vitest/issues) for searching known bugs and workarounds +- The [Discord community](https://chat.vitest.dev) for real-time help from other Vitest users and maintainers + + diff --git a/docs/guide/learn/matchers.md b/docs/guide/learn/matchers.md new file mode 100644 index 000000000..d6c31bc92 --- /dev/null +++ b/docs/guide/learn/matchers.md @@ -0,0 +1,241 @@ +--- +title: Using Matchers | Guide +prev: + text: Writing Tests + link: /guide/learn/writing-tests +next: + text: Testing Asynchronous Code + link: /guide/learn/async +--- + +# Using Matchers + +Vitest uses `expect` with "matchers" to assert that values meet certain conditions. This page covers the matchers you'll use most often. For the complete list, see the [Expect API Reference](/api/expect). + +## Common Matchers + +The simplest way to test a value is with exact equality. When you write `expect(2 + 2).toBe(4)`, the [`toBe`](/api/expect#tobe) matcher checks that the value is exactly `4` using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is). + +```js +import { expect, test } from 'vitest' + +test('two plus two is four', () => { + expect(2 + 2).toBe(4) +}) +``` + +This works great for primitive values like numbers, strings, and booleans. But when you're comparing objects, `toBe` checks *identity* (whether they're the exact same object in memory), not whether they have the same shape. That's where [`toEqual`](/api/expect#toequal) comes in. It recursively compares every field of an object or element of an array, ignoring object identity: + +```js +test('object assignment', () => { + const data = { one: 1 } + data.two = 2 + + expect(data).toEqual({ one: 1, two: 2 }) +}) +``` + +Here's an example that shows the difference more clearly. Two objects with the same content are `toEqual` but not `toBe`: + +```js +test('toBe vs toEqual', () => { + const a = { name: 'Alice' } + const b = { name: 'Alice' } + + // These are different objects in memory + expect(a).not.toBe(b) + + // But they have the same structure + expect(a).toEqual(b) +}) +``` + +::: tip +A good rule of thumb: use `toBe` for primitives (numbers, strings, booleans) and `toEqual` for objects and arrays. +::: + +You can also negate any matcher by inserting `.not` before it. This is useful when you want to verify that something is *not* the case: + +```js +test('adding positive numbers is not zero', () => { + expect(1 + 2).not.toBe(0) +}) +``` + +## Truthiness + +In tests you sometimes need to distinguish between `undefined`, `null`, and `false`. Other times you don't care about the exact value and just want to know if something is truthy or falsy. Vitest provides matchers for both situations: + +- [`toBeNull`](/api/expect#tobenull) matches only `null` +- [`toBeUndefined`](/api/expect#tobeundefined) matches only `undefined` +- [`toBeDefined`](/api/expect#tobedefined) is the opposite of `toBeUndefined`. It passes for anything that isn't `undefined` +- [`toBeTruthy`](/api/expect#tobetruthy) matches anything that an `if` statement would treat as true +- [`toBeFalsy`](/api/expect#tobefalsy) matches anything that an `if` statement would treat as false + +You should pick the matcher that most precisely describes what you're checking. Using `toBeTruthy` when you really mean `toBeDefined` can hide bugs, because `0` and `""` are both defined but falsy. + +```js +test('null checks', () => { + const n = null + + expect(n).toBeNull() + expect(n).toBeDefined() + expect(n).toBeFalsy() + expect(n).not.toBeTruthy() + expect(n).not.toBeUndefined() +}) + +test('zero', () => { + const z = 0 + + expect(z).toBeDefined() // passes: 0 is defined + expect(z).toBeFalsy() // passes: 0 is falsy + expect(z).not.toBeNull() // passes: 0 is not null +}) +``` + +## Numbers + +Most number comparisons are straightforward. Vitest provides the matchers you'd expect for greater-than, less-than, and equality checks: + +```js +test('number comparisons', () => { + const value = 2 + 2 + + expect(value).toBeGreaterThan(3) + expect(value).toBeGreaterThanOrEqual(3.5) + expect(value).toBeLessThan(5) + expect(value).toBeLessThanOrEqual(4.5) + + // For exact equality, both toBe and toEqual work the same for numbers + expect(value).toBe(4) + expect(value).toEqual(4) +}) +``` + +There is one common gotcha with floating point arithmetic. In JavaScript, `0.1 + 0.2` doesn't equal `0.3` exactly (it's `0.30000000000000004`). This means a `toBe(0.3)` check will fail. Use [`toBeCloseTo`](/api/expect#tobecloseto) instead, which compares numbers within a small rounding error: + +```js +test('adding floating point numbers', () => { + const value = 0.1 + 0.2 + + // This won't work because of floating point rounding + // expect(value).toBe(0.3) + + // This works + expect(value).toBeCloseTo(0.3) +}) +``` + +## Strings + +You can test strings against regular expressions with [`toMatch`](/api/expect#tomatch). This is especially handy when you care about a pattern rather than an exact value, like checking that an error message contains a certain word or that a URL matches a particular format: + +```js +test('there is no I in team', () => { + expect('team').not.toMatch(/I/) +}) + +test('version string matches semver format', () => { + expect('vitest@1.0.0').toMatch(/vitest@\d+\.\d+\.\d+/) +}) +``` + +## Arrays and Iterables + +[`toContain`](/api/expect#tocontain) checks that an array (or any iterable, like a `Set`) includes a particular item. It uses `===` for comparison, so it works well for primitives: + +```js +test('the shopping list has milk in it', () => { + const shoppingList = ['milk', 'bread', 'eggs', 'butter'] + + expect(shoppingList).toContain('milk') + expect(new Set(shoppingList)).toContain('milk') +}) +``` + +If you need to check that an array contains an object with a particular structure, use [`toContainEqual`](/api/expect#tocontainequal) instead. It works like `toEqual` but for individual items inside an array. + +## Objects + +When testing objects, you often want to check only a few important fields without specifying every property. [`toMatchObject`](/api/expect#tomatchobject) lets you do exactly that. It verifies that the object contains at least the properties you specify, and ignores any additional ones: + +```js +test('user has expected fields', () => { + const user = { + id: 1, + name: 'Alice', + email: 'alice@example.com', + createdAt: '2024-01-01' + } + + // We only care about name and email here + expect(user).toMatchObject({ + name: 'Alice', + email: 'alice@example.com', + }) +}) +``` + +For checking individual properties, especially nested ones, [`toHaveProperty`](/api/expect#tohaveproperty) is more readable. You pass a dot-separated path and optionally an expected value: + +```js +test('object has property', () => { + const user = { + name: 'Alice', + address: { city: 'Paris', zip: '75001' } + } + + expect(user).toHaveProperty('name') + expect(user).toHaveProperty('name', 'Alice') + expect(user).toHaveProperty('address.city', 'Paris') + expect(user).toHaveProperty('address.zip') +}) +``` + +## Exceptions + +To verify that a function throws an error, use [`toThrow`](/api/expect#tothrow). You need to wrap the call in another function so that Vitest can catch the error instead of letting it crash the test: + +```js +function compileCode(code) { + if (code === '') { + throw new Error('Cannot compile empty string') + } + return code +} + +test('compiling an empty string throws', () => { + // Check that it throws at all + expect(() => compileCode('')).toThrow() + + // Check the error message + expect(() => compileCode('')).toThrow('Cannot compile empty string') + + // Check the message with a regex + expect(() => compileCode('')).toThrow(/empty string/) +}) +``` + +::: tip +The wrapping function `() => compileCode('')` is important. If you wrote `expect(compileCode('')).toThrow()`, the error would be thrown *before* `expect` gets a chance to catch it, and the test would fail with an unhandled error instead. +::: + +## Soft Assertions + +Normally, a failing assertion stops the test immediately. That's useful most of the time, but sometimes you want to check several independent things and see all the failures at once rather than fixing them one by one. + +[`expect.soft`](/api/expect#soft) does exactly that. It records the failure but lets the test keep running: + +```js +test('check multiple fields', () => { + const user = { name: 'Alice', age: 30, role: 'admin' } + + expect.soft(user.name).toBe('Alice') + expect.soft(user.age).toBe(25) // this fails but execution continues + expect.soft(user.role).toBe('admin') + // the test report will show that age didn't match +}) +``` + +This is especially useful for validating the shape of an API response or a complex object where multiple fields might be wrong at the same time. diff --git a/docs/guide/learn/mock-functions.md b/docs/guide/learn/mock-functions.md new file mode 100644 index 000000000..593ae4cef --- /dev/null +++ b/docs/guide/learn/mock-functions.md @@ -0,0 +1,283 @@ +--- +title: Mock Functions | Guide +prev: + text: Setup and Teardown + link: /guide/learn/setup-teardown +next: + text: Snapshot Testing + link: /guide/learn/snapshots +--- + +# Mock Functions + +When writing tests, you often need to replace a real function or module with a controlled version. This is called **mocking**. There are several reasons you might want to do this: maybe the real function makes network requests that would slow down your tests, or maybe you need to simulate an error that's hard to trigger with real code. Mock functions let you control what a dependency returns, observe how it was called, and isolate the code under test from side effects. + +Vitest provides mocking utilities through the [`vi`](/api/vi) object. + +## Creating Mock Functions + +The simplest way to create a mock is with [`vi.fn()`](/api/vi#vi-fn). This gives you a function that does nothing by default (returns `undefined`), but tracks every call made to it: + +```js +import { expect, test, vi } from 'vitest' + +test('mock function basics', () => { + const getApples = vi.fn() + + // Call it + getApples() + + // Check it was called + expect(getApples).toHaveBeenCalled() + expect(getApples).toHaveBeenCalledTimes(1) + + // By default, a mock returns undefined + expect(getApples()).toBeUndefined() +}) +``` + +## Mock Return Values + +A mock that always returns `undefined` isn't very useful on its own. You'll usually want to control what it returns so you can test how your code reacts to different values: + +```js +import { expect, test, vi } from 'vitest' + +test('mock return values', () => { + const getApples = vi.fn() + + // Always return this value + getApples.mockReturnValue(10) + expect(getApples()).toBe(10) + + // Return this value only once, then fall back to the default + getApples.mockReturnValueOnce(20) + expect(getApples()).toBe(20) // 20 (one-time) + expect(getApples()).toBe(10) // back to default +}) +``` + +If the function you're mocking is async, use [`mockResolvedValue`](/api/mock#mockresolvedvalue) and [`mockRejectedValue`](/api/mock#mockrejectedvalue) to control the promise outcome: + +```js +test('mock async return values', async () => { + const fetchUser = vi.fn() + + fetchUser.mockResolvedValue({ name: 'Alice' }) + const user = await fetchUser() + expect(user.name).toBe('Alice') + + fetchUser.mockRejectedValue(new Error('Not found')) + await expect(fetchUser()).rejects.toThrow('Not found') +}) +``` + +## Mock Implementation + +Sometimes you need more than a fixed return value. You want the mock to actually do something with its arguments. [`mockImplementation`](/api/mock#mockimplementation) lets you provide a full replacement function: + +```js +import { expect, test, vi } from 'vitest' + +test('mock with custom implementation', () => { + const add = vi.fn() + add.mockImplementation((a, b) => a + b) + + expect(add(1, 2)).toBe(3) + expect(add(10, 20)).toBe(30) +}) +``` + +As a shorthand, you can pass the implementation directly to `vi.fn()`: + +```js +const add = vi.fn((a, b) => a + b) +``` + +## Inspecting Calls + +One of the most powerful things about mock functions is that they remember every call made to them. You can assert on how many times a function was called, what arguments it received, and what it returned: + +```js +import { expect, test, vi } from 'vitest' + +test('inspecting mock calls', () => { + const greet = vi.fn() + + greet('Alice') + greet('Bob', 'Charlie') + + // Number of calls + expect(greet).toHaveBeenCalledTimes(2) + + // Check specific arguments + expect(greet).toHaveBeenCalledWith('Alice') + expect(greet).toHaveBeenCalledWith('Bob', 'Charlie') + + // Access the raw call data + expect(greet.mock.calls).toEqual([ + ['Alice'], + ['Bob', 'Charlie'], + ]) +}) +``` + +The `.mock` property gives you full access to the call history. In addition to `.mock.calls`, you can also inspect `.mock.results` to see what the mock returned (or threw) on each call: + +```js +const double = vi.fn(x => x * 2) + +double(5) +double(10) + +expect(double.mock.results).toEqual([ + { type: 'return', value: 10 }, + { type: 'return', value: 20 }, +]) +``` + +::: warning +`.mock.calls` stores references to the arguments, not copies. If you pass an object to a mock and then mutate it afterwards, the recorded call will reflect the mutated state, not the state at the time of the call: + +```js +const fn = vi.fn() +const obj = { count: 1 } + +fn(obj) +obj.count = 2 + +// ❌ This fails! mock.calls[0][0].count is now 2, not 1 +expect(fn).toHaveBeenCalledWith({ count: 1 }) +``` + +If you need to assert on the original values, you can use `mockImplementation` to capture a clone at call time: + +```js +const calls = [] +const fn = vi.fn((obj) => { + calls.push(structuredClone(obj)) +}) + +const obj = { count: 1 } +fn(obj) +obj.count = 2 + +expect(calls[0]).toEqual({ count: 1 }) // ✅ passes +``` + +Alternatively, you can make your assertion before the mutation happens. +::: + +## Spying on Methods + +[`vi.spyOn`](/api/vi#vi-spyon) is different from `vi.fn()` in an important way. Instead of creating a brand new function, it wraps an *existing* method on an object. The original implementation still works by default, but you can observe every call and optionally override the behavior: + +```js +import { expect, test, vi } from 'vitest' + +const calculator = { + add(a, b) { + return a + b + }, +} + +test('spy on a method', () => { + const spy = vi.spyOn(calculator, 'add') + + // The original implementation still works + expect(calculator.add(1, 2)).toBe(3) + + // But we can observe calls + expect(spy).toHaveBeenCalledWith(1, 2) + expect(spy).toHaveBeenCalledTimes(1) +}) + +test('spy can override implementation', () => { + const spy = vi.spyOn(calculator, 'add') + spy.mockReturnValue(42) + + expect(calculator.add(1, 2)).toBe(42) +}) +``` + +This is particularly useful when you want to verify that your code calls a method correctly without replacing the method's behavior entirely. + +## Resetting Mocks + +Mock functions accumulate state as tests run. They remember every call, every return value, and any custom implementation you've set. If you don't reset them between tests, this state can leak and cause confusing failures. Vitest provides three levels of cleanup: + +- **[`mockClear()`](/api/mock#mockclear)** clears the recorded call history and return values, but keeps any custom implementation you've set +- **[`mockReset()`](/api/mock#mockreset)** does everything `mockClear` does, and also removes any custom implementation, returning the mock to its default state +- **[`mockRestore()`](/api/mock#mockrestore)** is specifically for spies created with `vi.spyOn`. It restores the original object method, effectively undoing the spy. On `vi.fn()` mocks, it behaves the same as `mockReset` + +In practice, the easiest approach is to restore all mocks automatically after each test: + +```js +import { afterEach, expect, test, vi } from 'vitest' + +const calculator = { + add: (a, b) => a + b, +} + +afterEach(() => { + vi.restoreAllMocks() +}) + +test('spy is restored after the test', () => { + const spy = vi.spyOn(calculator, 'add').mockReturnValue(42) + expect(calculator.add(1, 2)).toBe(42) + // afterEach will restore calculator.add to the original implementation +}) +``` + +Even better, you can configure this globally with the [`restoreMocks`](/config/restoremocks) option so you don't need the `afterEach` at all: + +```js [vitest.config.js] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + restoreMocks: true, + }, +}) +``` + +## Mocking Modules + +Sometimes you need to replace an [entire module](/guide/mocking/modules) rather than a single function. For example, a database client or a logger that you don't want running during tests. [`vi.mock`](/api/vi#vi-mock) lets you replace a module's exports with mock implementations: + +```js +import { expect, test, vi } from 'vitest' +import { getUser } from './db.js' + +vi.mock(import('./db.js'), () => ({ + getUser: vi.fn(), +})) + +test('mock a module', () => { + vi.mocked(getUser).mockReturnValue({ name: 'Alice' }) + + const user = getUser(1) + expect(user.name).toBe('Alice') + expect(getUser).toHaveBeenCalledWith(1) +}) +``` + +::: warning +[`vi.mock`](/api/vi#vi-mock) calls are hoisted to the top of the file. They run before any imports. This means the mocked version is in place by the time your test code runs. +::: + +::: tip +Notice that we pass `import('./db.js')` instead of a plain string `'./db.js'`. When you use `import()`, TypeScript can infer the module's types, so the factory function's return value is type-checked and `importOriginal` returns the correctly typed module. As a bonus, if you rename or move the file in your IDE, the import path will be updated automatically. If you use a string, you lose both the type safety and the automatic refactoring. +::: + +Vitest has comprehensive guides for specific mocking scenarios: + +- [Mocking Functions](/guide/mocking/functions) +- [Mocking Modules](/guide/mocking/modules) +- [Mocking Timers](/guide/mocking/timers) +- [Mocking Dates](/guide/mocking/dates) +- [Mocking Globals](/guide/mocking/globals) +- [Mocking Requests](/guide/mocking/requests) +- [Mocking the File System](/guide/mocking/file-system) +- [Mocking Classes](/guide/mocking/classes) diff --git a/docs/guide/learn/setup-teardown.md b/docs/guide/learn/setup-teardown.md new file mode 100644 index 000000000..49c901b5c --- /dev/null +++ b/docs/guide/learn/setup-teardown.md @@ -0,0 +1,241 @@ +--- +title: Setup and Teardown | Guide +prev: + text: Testing Asynchronous Code + link: /guide/learn/async +next: + text: Mock Functions + link: /guide/learn/mock-functions +--- + +# Setup and Teardown + +Often while writing tests, you need to do some work before tests run (initialize data, connect to a database, start a server) and clean up afterwards. Rather than duplicating this code in every test, Vitest provides lifecycle hooks that run automatically at the right time. + +## Repeating Setup for Each Test + +The most common hooks are [`beforeEach`](/api/hooks#beforeeach) and [`afterEach`](/api/hooks#aftereach). As the names suggest, `beforeEach` runs before every test in the file, and `afterEach` runs after every test, even if the test fails. This makes them perfect for ensuring each test starts with a known state. + +```js +import { afterEach, beforeEach, expect, test } from 'vitest' + +let items + +beforeEach(() => { + items = ['apple', 'banana', 'cherry'] +}) + +afterEach(() => { + items = [] +}) + +test('items starts with 3 fruits', () => { + expect(items).toHaveLength(3) +}) + +test('can add an item', () => { + items.push('date') + expect(items).toHaveLength(4) + // afterEach will reset items for the next test, + // so this mutation won't leak into other tests +}) +``` + +Without these hooks, the second test's `push` would affect any test that runs after it, which is a classic source of flaky tests. The hooks guarantee clean state for every test. + +## One-Time Setup + +Some setup is too expensive to repeat for every test. If you need to connect to a database, start a server, or load a large file, doing that before every test would slow your suite down dramatically. That's what [`beforeAll`](/api/hooks#beforeall) and [`afterAll`](/api/hooks#afterall) are for. They run once for the entire file: + +```js +import { afterAll, beforeAll, expect, test } from 'vitest' + +let db + +beforeAll(async () => { + db = await connectToDatabase() +}) + +afterAll(async () => { + await db.close() +}) + +test('can query users', async () => { + const users = await db.query('SELECT * FROM users') + expect(users.length).toBeGreaterThan(0) +}) + +test('can query products', async () => { + const products = await db.query('SELECT * FROM products') + expect(products.length).toBeGreaterThan(0) +}) +``` + +The database connection is created once, shared across all tests, and then closed when the file finishes running. + +## Scoping with `describe` + +Hooks defined inside a `describe` block only apply to the tests within that block. Top-level hooks apply to every test in the file. This lets you set up different state for different groups of tests: + +```js +import { beforeEach, describe, expect, test } from 'vitest' + +describe('math operations', () => { + let value + + beforeEach(() => { + value = 0 + }) + + test('can add', () => { + value += 5 + expect(value).toBe(5) + }) + + test('can subtract', () => { + value -= 3 + expect(value).toBe(-3) // value was reset to 0 by beforeEach + }) +}) + +describe('string operations', () => { + let text + + beforeEach(() => { + text = 'hello' + }) + + test('can uppercase', () => { + expect(text.toUpperCase()).toBe('HELLO') + }) +}) +``` + +Each `describe` block has its own `beforeEach` that only affects the tests inside it. The string tests don't know or care about the `value` variable, and vice versa. + +## Execution Order + +When you have hooks at multiple levels, it's helpful to understand the order they run in. Top-level hooks wrap around inner hooks, forming a nesting structure: + +```js +import { afterAll, afterEach, beforeAll, beforeEach, describe, test } from 'vitest' + +beforeAll(() => console.log('1 - beforeAll')) +afterAll(() => console.log('6 - afterAll')) +beforeEach(() => console.log('2 - beforeEach')) +afterEach(() => console.log('4 - afterEach')) + +describe('suite', () => { + beforeEach(() => console.log('3 - inner beforeEach')) + afterEach(() => console.log('3.5 - inner afterEach')) + + test('example', () => { + console.log(' test') + }) +}) +``` + +This produces the following output: + +``` +1 - beforeAll +2 - beforeEach +3 - inner beforeEach + test +3.5 - inner afterEach +4 - afterEach +6 - afterAll +``` + +Notice the pattern: outer `beforeEach` runs first (setting up the broadest context), then inner `beforeEach` runs (narrowing the context). After the test, the order reverses: inner `afterEach` cleans up the narrow context first, then outer `afterEach` handles the broader cleanup. + +## Cleanup with `onTestFinished` + +Sometimes you create a resource inside a test that needs to be cleaned up afterwards. You could use `afterEach`, but that means the cleanup is separated from the setup, which can make the test harder to follow. [`onTestFinished`](/api/hooks#ontestfinished) lets you register a cleanup function right where you create the resource: + +```js +import { expect, onTestFinished, test } from 'vitest' + +test('creates a temporary file', () => { + const file = createTempFile() + onTestFinished(() => { + deleteTempFile(file) + }) + + expect(file.exists()).toBe(true) +}) +``` + +A similar pattern works with `beforeEach`. You can return a cleanup function and Vitest will call it after each test. This is especially nice when the setup and teardown are closely related: + +```js +import { beforeEach } from 'vitest' + +beforeEach(() => { + const server = startServer() + return () => { + server.close() + } +}) +``` + +## Fixtures with `test.extend` + +The examples above use `let` variables and `beforeEach` to set up shared state. This works, but it has some downsides: the variable declarations are separated from the initialization, the types require explicit annotation, and it's easy to forget to clean up. + +Vitest offers a better pattern for this with [`test.extend`](/guide/test-context#extend-test-context). You define reusable **fixtures** that are automatically created for each test and cleaned up afterwards: + +```js [my-test.js] +import { test as baseTest } from 'vitest' + +export const test = baseTest + .extend('db', async ({}, { onCleanup }) => { + const db = await createDatabase() + onCleanup(() => db.close()) + return db + }) + .extend('user', async ({ db }) => { + return await db.createUser({ name: 'Alice' }) + }) +``` + +```js [my-test.test.js] +import { expect } from 'vitest' +import { test } from './my-test.js' + +test('user is created', ({ db, user }) => { + expect(user.name).toBe('Alice') +}) +``` + +Fixtures are only initialized when a test actually uses them (by destructuring them from the context), and they can depend on each other. This makes them a great alternative to `beforeEach`/`afterEach` for most setup and teardown patterns. + +See the [Test Context](/guide/test-context) guide for the full details on fixtures, scoping, and overrides. + +## Setup Files + +If you have setup code that should run before every test file in your project (things like polyfills, global configuration, or custom matchers), you can put it in a setup file and point to it with the [`setupFiles`](/config/setupfiles) config option: + +```js [vitest.config.js] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + setupFiles: ['./test/setup.js'], + }, +}) +``` + +```js [test/setup.js] +// This runs before every test file +import { expect } from 'vitest' +import { customMatchers } from './custom-matchers.js' + +expect.extend(customMatchers) +``` + +Unlike `beforeAll`, which runs once per file, setup files run in a separate phase before the test file even starts being collected. This makes them the right place for things like extending the `expect` API or configuring global polyfills. + +::: tip +For advanced cases where your test needs to run *inside* a wrapping context (like a database transaction or a tracing span), see the [`aroundEach`](/api/hooks#aroundeach) and [`aroundAll`](/api/hooks#aroundall) hooks. For the complete lifecycle picture, see [Test Run Lifecycle](/guide/lifecycle). +::: diff --git a/docs/guide/learn/snapshots.md b/docs/guide/learn/snapshots.md new file mode 100644 index 000000000..cf7445571 --- /dev/null +++ b/docs/guide/learn/snapshots.md @@ -0,0 +1,141 @@ +--- +title: Snapshot Testing | Guide +prev: + text: Mock Functions + link: /guide/learn/mock-functions +next: + text: Testing in Practice + link: /guide/learn/testing-in-practice +--- + +# Snapshot Testing + +Snapshot tests capture the output of a piece of code and save it to a file. On subsequent runs, the output is compared against the saved snapshot. If the output changes, the test fails. Either the change is a bug, or the snapshot needs to be updated. + +This approach is particularly useful when you're testing something that produces structured output: a function that returns a complex object, a component that renders HTML, or an error formatter that produces multi-line messages. Writing manual assertions for every field or line would be tedious and fragile. Instead, you capture the entire output once, and let Vitest tell you if it ever changes. + +## Your First Snapshot + +To create a snapshot test, pass a value to [`toMatchSnapshot()`](/api/expect#tomatchsnapshot): + +```js +import { expect, test } from 'vitest' + +function generateGreeting(name) { + return { + message: `Hello, ${name}!`, + timestamp: null, + version: 2, + } +} + +test('generates a greeting', () => { + expect(generateGreeting('Alice')).toMatchSnapshot() +}) +``` + +The first time you run this test, there's no existing snapshot to compare against, so Vitest creates one. It stores the snapshot in a `__snapshots__` directory next to your test file: + +``` +__snapshots__/ + example.test.js.snap +``` + +If you open that file, you'll see a serialized representation of the value: + +```js +exports['generates a greeting 1'] = ` +{ + "message": "Hello, Alice!", + "timestamp": null, + "version": 2, +} +` +``` + +From now on, every time you run this test, Vitest serializes the output of `generateGreeting('Alice')` and compares it character-by-character against this stored snapshot. If the output changes (say, someone modifies the message format or bumps the version number), the test fails and shows a clear diff of what changed. + +::: tip +Commit your snapshot files to version control. They serve as a record of the expected output and should be reviewed in code review just like any other test assertion. +::: + +## Inline Snapshots + +External snapshot files work well, but they mean you have to jump to a different file to see what the expected output actually looks like. For smaller values, it's often more convenient to keep the snapshot right in your test file with [`toMatchInlineSnapshot()`](/api/expect#tomatchinlinesnapshot). + +Start by writing the assertion without any argument: + +```js +test('generates a greeting', () => { + expect(generateGreeting('Alice')).toMatchInlineSnapshot() +}) +``` + +When you run the test, Vitest will **automatically fill in** the snapshot as a string argument: + +```js +test('generates a greeting', () => { + expect(generateGreeting('Alice')).toMatchInlineSnapshot(` + { + "message": "Hello, Alice!", + "timestamp": null, + "version": 2, + } + `) +}) +``` + +Now the expected output lives right next to the code that produces it. You can read the test and immediately understand what `generateGreeting` is expected to return. When the output changes, Vitest updates the string in place, so you don't need to manage separate snapshot files. + +Inline snapshots are great for small, focused values. For large outputs (like a full HTML page), external snapshots or file snapshots are a better fit. + +## Updating Snapshots + +When you intentionally change the output of your code, existing snapshots will be outdated and the tests will fail. This is by design; it's the whole point of snapshot testing. But once you've verified that the new output is correct, you need to update the snapshots. + +There are several ways to do this: + +- **In watch mode**: press `u` in the terminal to update all failed snapshots +- **From the CLI**: run `vitest -u` or `vitest --update` to update snapshots and exit +- **In VS Code**: use the "Update Snapshots" command on the test gutter icon from the [Vitest extension](https://vitest.dev/vscode) + +```bash +vitest -u +``` + +For inline snapshots, Vitest modifies your test file directly with the new values. For external snapshots, it rewrites the `.snap` file. + +::: warning +Be careful when updating snapshots. Always review the diff to confirm the changes are intentional and not a bug. It's easy to accidentally accept a broken output by blindly pressing `u`. +::: + +## File Snapshots + +Sometimes the output you're testing is large enough that even an external `.snap` file feels awkward, or you want to view the snapshot with proper syntax highlighting in your editor. [`toMatchFileSnapshot()`](/api/expect#tomatchfilesnapshot) lets you save the snapshot to a file with any extension you want: + +```js +test('renders the component', async () => { + const html = renderComponent() + await expect(html).toMatchFileSnapshot('./fixtures/component.html') +}) +``` + +The snapshot is stored as a plain `.html` file that you can open in a browser, view with syntax highlighting, or diff with standard tools. This works well for HTML, SVG, CSS, generated code, or any output where the file format matters for readability. + +## When to Use Snapshots + +Snapshots shine when you're working with structured, serializable output that would be painful to assert on manually. Some common use cases: + +- A function that returns a complex configuration object with many nested fields +- HTML or markup generated by a rendering function or template engine +- Error messages that include formatted stack traces or context information +- CLI output or log messages with specific formatting +- JSON API responses where you want to catch any unexpected field changes + +On the other hand, snapshots are not always the best tool. If the output changes frequently (for instance, it includes timestamps or random IDs), you'll spend more time updating snapshots than they save you. And if you only care about one or two specific fields, a targeted assertion like [`toMatchObject`](/api/expect#tomatchobject) or [`toHaveProperty`](/api/expect#tohaveproperty) expresses your intent more clearly than a snapshot that captures everything. + +The general rule: use snapshots when you want to protect against *any* change in the output, and use targeted assertions when you only care about *specific* properties. + +::: tip +For custom snapshot serializers, snapshot matchers, and advanced configuration, see the [Snapshot](/guide/snapshot) guide. +::: diff --git a/docs/guide/learn/snippets/debug-output-fail.ansi b/docs/guide/learn/snippets/debug-output-fail.ansi new file mode 100644 index 000000000..ee3d9d8e7 --- /dev/null +++ b/docs/guide/learn/snippets/debug-output-fail.ansi @@ -0,0 +1,19 @@ + FAIL src/user.test.js > createUser > sets the default role +AssertionError: expected { name: 'Alice', role: 'viewer' } to deeply equal { name: 'Alice', role: 'member' } + +- Expected ++ Received + + { + "name": "Alice", +- "role": "member", ++ "role": "viewer", + } + + ❯ src/user.test.js:8:22 + 6| test('sets the default role', () => { + 7| const user = createUser('Alice') + 8| expect(user).toEqual({ name: 'Alice', role: 'member' }) + ^ + 9| }) + 10| }) diff --git a/docs/guide/learn/snippets/test-output-fail.ansi b/docs/guide/learn/snippets/test-output-fail.ansi new file mode 100644 index 000000000..257515ee0 --- /dev/null +++ b/docs/guide/learn/snippets/test-output-fail.ansi @@ -0,0 +1,16 @@ + FAIL src/utils.test.js > Math.sqrt > returns the square root of perfect squares +AssertionError: expected 3 to be 2 + +- Expected ++ Received + + 2 + 3 + + ❯ src/utils.test.js:5:28 + 3| test('returns the square root of perfect squares', () => { + 4| expect(Math.sqrt(4)).toBe(2) + 5| expect(Math.sqrt(9)).toBe(2) + ^ + 6| }) + 7| diff --git a/docs/guide/learn/snippets/test-output-multiple.ansi b/docs/guide/learn/snippets/test-output-multiple.ansi new file mode 100644 index 000000000..8cb7961aa --- /dev/null +++ b/docs/guide/learn/snippets/test-output-multiple.ansi @@ -0,0 +1,6 @@ + ✓ src/utils.test.js (3 tests) 5ms + ✓ src/math.test.js (2 tests) 3ms + ✓ src/strings.test.js (4 tests) 7ms + + Test Files 3 passed (3) +  Tests 9 passed (9) diff --git a/docs/guide/learn/snippets/test-output-single.ansi b/docs/guide/learn/snippets/test-output-single.ansi new file mode 100644 index 000000000..934b32f82 --- /dev/null +++ b/docs/guide/learn/snippets/test-output-single.ansi @@ -0,0 +1,8 @@ + ✓ src/utils.test.js (3 tests) 5ms + ✓ Math.sqrt 4ms + ✓ returns the square root of perfect squares 2ms + ✓ returns NaN for negative numbers 1ms + ✓ returns 0 for 0 1ms + + Test Files 1 passed (1) +  Tests 3 passed (3) diff --git a/docs/guide/learn/testing-in-practice.md b/docs/guide/learn/testing-in-practice.md new file mode 100644 index 000000000..9f2283ae5 --- /dev/null +++ b/docs/guide/learn/testing-in-practice.md @@ -0,0 +1,436 @@ +--- +title: Testing in Practice | Guide +prev: + text: Snapshot Testing + link: /guide/learn/snapshots +next: + text: Debugging Tests + link: /guide/learn/debugging-tests +--- + +# Testing in Practice + +The previous pages covered the Vitest API: assertions, mocking, snapshots, and test lifecycle hooks. This page focuses on applying those tools to real code. It covers how to decide what to test, how to structure tests effectively, and how to organize test files as a project grows. + +## What to Test + +When you sit down to write tests for a function or module, start by thinking about its **contract**: what does it promise to do for the code that calls it? The contract is defined by its inputs (arguments, configuration) and its outputs (return values, side effects, errors). These are the things your tests should verify. + +Consider a `formatPrice` function: + +```js [formatPrice.js] +export function formatPrice(amount, currency) { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency, + }).format(amount) +} +``` + +The contract here is: given an amount and a currency code, return a formatted price string. Good tests for this function would cover: + +```js [formatPrice.test.js] +import { expect, test } from 'vitest' +import { formatPrice } from './formatPrice.js' + +test('formats USD prices', () => { + expect(formatPrice(10, 'USD')).toBe('$10.00') +}) + +test('formats EUR prices', () => { + expect(formatPrice(10, 'EUR')).toMatchInlineSnapshot(`"€10.00"`) +}) + +test('handles zero', () => { + expect(formatPrice(0, 'USD')).toBe('$0.00') +}) + +test('handles negative amounts', () => { + expect(formatPrice(-5.5, 'USD')).toBe('-$5.50') +}) + +test('rounds to two decimal places', () => { + expect(formatPrice(10.999, 'USD')).toBe('$11.00') +}) +``` + +Notice what these tests *don't* do. They don't check which internal `Intl.NumberFormat` options were passed, or whether an intermediate variable was set. They only check the output. + +::: tip +A good rule of thumb: if someone refactors the internals but the output stays the same, should the test break? If it would, you're probably testing implementation details rather than behavior. +::: + +## Structuring a Test + +Most tests follow a natural three-part structure, sometimes called "Arrange, Act, Assert": + +1. **Set up** the data your test needs +2. **Call** the function or perform the action you're testing +3. **Check** that the result matches your expectations + +```js +test('removes an item from the list', () => { + // Set up + const list = new ShoppingList() + list.add('milk') + list.add('bread') + + // Act + list.remove('milk') + + // Check + expect(list.getItems()).toEqual(['bread']) +}) +``` + +You don't need comments labeling each section. The structure becomes natural once you've written a few tests. The important thing is keeping each test focused on one behavior. + +### One Behavior Per Test + +If you find yourself writing "and" in a test name ("formats price and handles errors and logs the result"), that's a sign you should split it into separate tests. + +### Descriptive Names + +Write test names that describe the behavior, not the implementation. "returns formatted price for USD" is better than "calls Intl.NumberFormat with correct options". When a test fails, the name should tell you what broke without having to read the test body. + +## Testing Edge Cases + +After covering the main behavior, think about the boundaries. What happens at the edges? What inputs are unusual but valid? What should happen when things go wrong? + +Here's an example with a `parseAge` function that takes user input and returns a number: + +```js [parseAge.js] +export function parseAge(input) { + const age = Number(input) + if (Number.isNaN(age) || age < 0 || age > 150) { + throw new Error(`Invalid age: ${input}`) + } + return Math.floor(age) +} +``` + +The happy path is straightforward, but the edge cases are where bugs hide: + +```js [parseAge.test.js] +import { expect, test } from 'vitest' +import { parseAge } from './parseAge.js' + +test('parses a valid age', () => { + expect(parseAge('25')).toBe(25) +}) + +test('rounds down decimal ages', () => { + expect(parseAge('25.9')).toBe(25) +}) + +test('handles zero', () => { + expect(parseAge('0')).toBe(0) +}) + +test('handles the upper boundary', () => { + expect(parseAge('150')).toBe(150) +}) + +test('throws for negative numbers', () => { + expect(() => parseAge('-1')).toThrow('Invalid age: -1') +}) + +test('throws for numbers above 150', () => { + expect(() => parseAge('151')).toThrow('Invalid age: 151') +}) + +test('throws for non-numeric strings', () => { + expect(() => parseAge('abc')).toThrow('Invalid age: abc') +}) + +test('throws for empty string', () => { + expect(() => parseAge('')).toThrow('Invalid age: ') +}) +``` + +You don't need to test every possible input. Focus on the boundaries (0, 150, 151, -1), the error paths, and the types of inputs your function might realistically receive. + +::: tip +If you're unsure whether an edge case matters, ask yourself: could a real user or a real caller trigger this? If yes, test it. +::: + +### Property-Based Testing + +For functions with a wide range of valid inputs, manually choosing edge cases can only go so far. **Property-based testing** is a technique where you describe the *properties* that should hold for any input, and the testing framework generates hundreds of random inputs to try to find one that breaks. + +For example, you might say "for any valid age string, `parseAge` should return a non-negative integer" and let the tool find the counterexample. [fast-check](https://fast-check.dev/) is a popular property-based testing library that integrates well with Vitest. It's an advanced technique, but worth knowing about as your testing needs grow. + +## When to Mock + +Mocking is a powerful tool, but it's easy to overuse. + +### Slow Dependencies + +Network requests, file system operations, and database calls can make your tests take seconds instead of milliseconds. Replace them with mocks to keep the feedback loop fast. + +For HTTP requests specifically, consider using [Mock Service Worker](https://mswjs.io/) instead of mocking fetch directly. See the [Mocking Requests](/guide/mocking/requests) guide for setup instructions. + +### Non-Deterministic Values + +If your code depends on the current date, a random number, or a UUID generator, mock those to make your tests predictable. Vitest provides [`vi.useFakeTimers()`](/api/vi#vi-usefaketimers) and [`vi.setSystemTime()`](/api/vi#vi-setsystemtime) for controlling time in tests. + +### What Not to Mock + +Don't mock the thing you're testing. If you're testing a `UserService`, don't mock the `UserService`. Mock its *dependencies* (the database, the email sender) and let the service itself run for real. + +Also, prefer real implementations when they're fast and reliable. If a dependency is a simple in-memory data structure or a pure function, there's no reason to mock it. The closer your tests are to real usage, the more confidence they give you. + +::: tip +Only reach for mocks when the real thing is slow, flaky, or has side effects you can't control in a test. +::: + +## Fixing Bugs with Tests + +When you find a bug, it's tempting to jump straight into the code and fix it. A better approach is to write a failing test first that reproduces the bug, then fix the code and watch the test turn green. + +This has several benefits. The test proves the bug is real and not just a misunderstanding. It documents exactly what was broken. And it prevents the same bug from coming back later, because the test will catch it if someone accidentally reintroduces the same problem. + +Here's what this looks like in practice. Suppose users report that `parseAge` crashes when given a string with leading spaces like `" 25"`. First, write a test that reproduces the problem: + +```js +test('handles leading spaces', () => { + expect(parseAge(' 25')).toBe(25) +}) +``` + +Run it and confirm it fails. Now you know exactly what's broken and have a clear target. Fix the implementation: + +```js +export function parseAge(input) { + const age = Number(input.trim()) + // ... +} +``` + +Run the test again. It passes. The bug is fixed, and you have a regression test that will catch it if someone removes the `.trim()` call later. + +::: tip +If you use AI agents to fix bugs, configure them to follow the same principle: reproduce the issue with a failing test first, then fix the code. This prevents the agent from "fixing" a bug by changing the test instead of the code, and gives you confidence that the fix actually works. +::: + +## Organizing Test Files + +There's no single right way to organize tests, but some patterns scale better than others. + +### File Layout + +The simplest starting point is one test file per source file. For every `utils.js`, there's a `utils.test.js` right next to it. This makes it easy to find the tests for any given piece of code, and most editors will show them side by side in the file tree: + +``` +src/ + utils.js + utils.test.js + formatPrice.js + formatPrice.test.js +``` + +Some teams prefer a separate `__tests__` or `test` directory instead. Either approach works. The important thing is consistency across the project. Vitest's [`include`](/config/include) pattern matches both layouts by default. + +### Grouping with `describe` + +When a module exports multiple functions, use `describe` blocks to group the tests for each one. This keeps the test output organized and makes it clear which function a failing test belongs to: + +```js +describe('formatPrice', () => { + test('formats USD prices', () => { /* ... */ }) + test('handles zero', () => { /* ... */ }) +}) + +describe('parseAmount', () => { + test('parses valid amounts', () => { /* ... */ }) + test('throws for invalid input', () => { /* ... */ }) +}) +``` + +Avoid nesting `describe` blocks more than one or two levels deep. Deeply nested test trees are hard to read and usually mean the source module is doing too many things at once. + +### Splitting Large Files + +As a project grows, some test files will inevitably get long. If a test file grows beyond a few hundred lines, consider splitting it by theme or feature area. For example, `userService.test.js` might become `userService.creation.test.js` and `userService.auth.test.js`. This also makes it faster to run a subset of tests during development. + +### Naming Tests + +Test names matter more than you might expect. When a test fails in CI, the name is often the first thing someone reads. Names like "works correctly" or "handles edge case" don't tell you what broke. + +Prefer names that describe the specific behavior: "returns 0 for an empty cart", "throws if the email format is invalid", "preserves existing items when adding a new one". The test output should read like a specification of what the module does. + +## A Worked Example + +Let's put it all together. Here's a small `TodoList` module: + +```js [todoList.js] +let nextId = 1 + +export function createTodoList() { + const items = [] + + return { + add(text) { + if (!text.trim()) { + throw new Error('Todo text cannot be empty') + } + const todo = { id: nextId++, text, completed: false } + items.push(todo) + return todo + }, + + remove(id) { + const index = items.findIndex(item => item.id === id) + if (index === -1) { + throw new Error(`Todo with id ${id} not found`) + } + items.splice(index, 1) + }, + + toggle(id) { + const todo = items.find(item => item.id === id) + if (!todo) { + throw new Error(`Todo with id ${id} not found`) + } + todo.completed = !todo.completed + }, + + getAll() { + return items + }, + + getCompleted() { + return items.filter(item => item.completed) + }, + } +} +``` + +Looking at this code, we can identify the behaviors to test: + +- Adding items (the main purpose) +- Adding empty items (should fail) +- Removing items by ID +- Removing items that don't exist (should fail) +- Toggling completion status +- Getting all items vs. completed items + +Here's how the test file might look: + +```js [todoList.test.js] +import { describe, expect, test } from 'vitest' +import { createTodoList } from './todoList.js' + +describe('add', () => { + test('adds a new todo', () => { + const list = createTodoList() + const todo = list.add('Buy groceries') + + expect(todo.text).toBe('Buy groceries') + expect(todo.completed).toBe(false) + expect(list.getAll()).toHaveLength(1) + }) + + test('assigns unique IDs to each todo', () => { + const list = createTodoList() + const first = list.add('First') + const second = list.add('Second') + + expect(first.id).not.toBe(second.id) + }) + + test('throws when text is empty', () => { + const list = createTodoList() + expect(() => list.add('')).toThrow('Todo text cannot be empty') + }) + + test('throws when text is only whitespace', () => { + const list = createTodoList() + expect(() => list.add(' ')).toThrow('Todo text cannot be empty') + }) +}) + +describe('remove', () => { + test('removes a todo by ID', () => { + const list = createTodoList() + const todo = list.add('Buy groceries') + + list.remove(todo.id) + + expect(list.getAll()).toHaveLength(0) + }) + + test('keeps other items when removing one', () => { + const list = createTodoList() + const first = list.add('First') + list.add('Second') + + list.remove(first.id) + + expect(list.getAll()).toHaveLength(1) + expect(list.getAll()[0].text).toBe('Second') + }) + + test('throws when ID does not exist', () => { + const list = createTodoList() + expect(() => list.remove(999)).toThrow('Todo with id 999 not found') + }) +}) + +describe('toggle', () => { + test('marks a todo as completed', () => { + const list = createTodoList() + const todo = list.add('Buy groceries') + + list.toggle(todo.id) + + expect(list.getAll()[0].completed).toBe(true) + }) + + test('toggles back to incomplete', () => { + const list = createTodoList() + const todo = list.add('Buy groceries') + + list.toggle(todo.id) + list.toggle(todo.id) + + expect(list.getAll()[0].completed).toBe(false) + }) + + test('throws when ID does not exist', () => { + const list = createTodoList() + expect(() => list.toggle(999)).toThrow('Todo with id 999 not found') + }) +}) + +describe('getCompleted', () => { + test('returns only completed todos', () => { + const list = createTodoList() + const buy = list.add('Buy groceries') + list.add('Clean house') + list.toggle(buy.id) + + const completed = list.getCompleted() + + expect(completed).toHaveLength(1) + expect(completed[0].text).toBe('Buy groceries') + }) + + test('returns empty array when nothing is completed', () => { + const list = createTodoList() + list.add('Buy groceries') + + expect(list.getCompleted()).toHaveLength(0) + }) +}) +``` + +Each `describe` block focuses on one method. Each test verifies one specific behavior. The test names read like a specification of what the module does. And if any of these tests fail, the name and the assertion will tell you exactly what broke. + +::: tip +Notice that we create a fresh `createTodoList()` in every test. This keeps tests independent, which means they can run in any order without affecting each other. If you find yourself repeating the same setup in every test, that's a good candidate for [`beforeEach`](/api/hooks#beforeeach) or a [`test.extend`](/guide/test-context#extend-test-context) fixture. +::: + +--- + +If you're building a web application and want to test components in a real browser environment, check out [Component Testing](/guide/browser/component-testing) for testing React, Vue, Svelte, and other UI frameworks. diff --git a/docs/guide/learn/writing-tests-with-ai.md b/docs/guide/learn/writing-tests-with-ai.md new file mode 100644 index 000000000..ae3c59f4e --- /dev/null +++ b/docs/guide/learn/writing-tests-with-ai.md @@ -0,0 +1,137 @@ +--- +title: Writing Tests with AI | Guide +prev: + text: Debugging Tests + link: /guide/learn/debugging-tests +next: + text: Why Browser Mode + link: /guide/browser/why +--- + +# Writing Tests with AI + +AI coding assistants can help you write tests faster, but the quality of the output depends heavily on what you put in. A vague prompt produces vague tests. A specific prompt with the right context produces tests that are actually worth keeping. + +This page covers how to get good test code from AI tools, and what to watch for when reviewing the results. + +## Providing Context + +The single most important thing you can do is give the AI enough context to understand what it's testing. + +Start with the source file itself. The AI needs to see the actual implementation, not just a description of what the function does. Include the full file, or at least the function you want tested along with its imports and types. + +Share existing test files from the same project. This helps the AI match your conventions: whether you use `test` or `it`, how you structure `describe` blocks, whether you prefer `test.extend` fixtures or `beforeEach`, and how you name your tests. AI tools are good at pattern matching, but they need patterns to match against. + +Include your Vitest config, especially if you've enabled [`globals`](/config/globals), set a custom [`environment`](/config/environment), or configured [`setupFiles`](/config/setupfiles). Without this context, the AI might generate unnecessary imports, use the wrong test environment, or miss setup that your tests depend on. + +If the code under test has dependencies that need mocking, share those files too (or at least their type signatures). The AI can't write a useful mock for a database client it's never seen. + +::: tip +If your project has an `AGENTS.md` or similar file with coding conventions, include that as well. Many AI tools pick up on these automatically and will follow the rules defined there. +::: + +## Writing Good Prompts + +Specific prompts produce better tests than generic ones. Compare these two: + +**Vague:** "Write tests for `userService.js`" + +This will produce tests, but they'll likely be shallow: one happy-path test per function, minimal edge case coverage, and generic test names. + +**Better:** "Write tests for the `createUser` function in `userService.js`. Cover validation errors (missing name, invalid email format, duplicate email), the successful creation path, and verify that the password is hashed before being stored." + +This tells the AI exactly which function to focus on, which scenarios matter, and what behavior to verify. The output will be more thorough and more relevant. + +### Tips for Better Prompts + +- Ask for edge cases explicitly. "Include tests for empty inputs, boundary values, and error handling" produces more comprehensive coverage than leaving it to the AI's judgment. Without this nudge, most tools will generate a handful of happy-path tests and stop there. +- Mention specific Vitest features if you want them used. "Use `toMatchInlineSnapshot` for the error messages" or "use `test.each` for the different currency formats" guides the AI toward the right tools instead of letting it fall back to repetitive copy-paste tests. +- If you're testing async code, say so. "The function returns a Promise" or "this calls an external API" helps the AI use `async`/`await` and appropriate matchers like `.resolves` and `.rejects`. +- Tell the AI what *not* to do. "Test against the real implementation, don't mock any modules" or "don't use snapshot tests" prevents common defaults you don't want. AI tools tend to over-mock, and an explicit constraint prevents that. +- Describe the test structure you want. "Group tests by method using `describe` blocks" or "use `test.extend` fixtures for the database connection instead of `beforeEach`" saves you from restructuring the output afterwards. +- Reference existing tests when asking for additions. "Follow the same style as the tests in `auth.test.js`" is more effective than describing the style from scratch. The AI will pick up on naming conventions, assertion patterns, and import styles from the example. +- If the first result isn't right, iterate. "These tests are too focused on implementation details. Rewrite them to only assert on the return values and thrown errors" is a valid follow-up. Refining through conversation often produces better results than trying to write the perfect prompt upfront. + +## Reviewing AI-Generated Tests + +AI-generated tests can look convincing at first glance but still have problems. Here's what to check before committing them. + +### Do the tests actually assert something meaningful? + +Watch for tests that call a function but only check that it doesn't throw, or tests that assert on the mock itself rather than the behavior. A test like this gives false confidence: + +```js +test('creates a user', () => { + const user = createUser('Alice', 'alice@example.com') + expect(user).toBeDefined() // this passes for almost anything +}) +``` + +A better assertion checks the actual properties: + +```js +test('creates a user with the correct fields', () => { + const user = createUser('Alice', 'alice@example.com') + expect(user).toMatchObject({ + name: 'Alice', + email: 'alice@example.com', + }) + expect(user.id).toBeTypeOf('string') +}) +``` + +### Are they testing behavior or implementation? + +AI tends to over-mock. If you see a test that mocks every dependency and then asserts that specific internal methods were called in a specific order, that's testing implementation details. These tests break every time you refactor, even if the behavior stays the same. + +Ask yourself: if someone changed the internals but the function still returned the correct result, would this test break? If yes, it's probably too coupled to the implementation. See [Testing in Practice](/guide/learn/testing-in-practice#what-to-test) for more on this distinction. + +### Do the tests actually run? + +Always run the tests before committing. AI-generated tests can have import errors, reference functions that don't exist, or use APIs incorrectly. A test that looks correct in a chat window might fail immediately when you actually execute it: + +```bash +vitest run src/userService.test.js +``` + +### Are there real edge cases? + +AI tools tend to generate happy-path tests and skip the hard cases. After reviewing the generated tests, ask yourself: what happens with empty input? What about `null` or `undefined`? What if the network request fails? What if the list is empty? + +If these scenarios aren't covered, ask the AI to add them, or write them yourself. + +## Iterating on the Output + +Treat AI-generated tests as a first draft, not a finished product. A good workflow looks like: + +1. **Generate** the initial tests with a specific prompt and good context +2. **Run** them immediately to catch errors +3. **Review** each test for the issues described above +4. **Ask for revisions** if entire sections need improvement ("these tests mock too much, rewrite them to test the actual integration with the database module") +5. **Edit manually** for small fixes rather than re-prompting for every detail + +Over time, as the AI sees more of your codebase and test patterns, its output will improve. The earlier tests in your project set the pattern for everything that follows, so it's worth getting those right. + +## Common Pitfalls + +### Wrong APIs + +The most frequent issue with AI-generated Vitest tests is using the wrong API surface. AI models are trained on a lot of Jest code, so they sometimes generate `jest.fn()` instead of `vi.fn()`, or `jest.mock` instead of `vi.mock`. These will fail immediately. + +A related problem is imports: if your config has `globals: true`, the AI might still add `import { test, expect } from 'vitest'` (harmless but unnecessary), or the reverse, generating tests without imports when globals aren't enabled. If you keep seeing Jest APIs, point the AI to the [Vitest API reference](/api/vi) or include it in the context. + +### Mock Cleanup + +AI-generated tests often set up spies with `vi.spyOn` or replace modules with `vi.mock` but never restore them. If your config doesn't have [`restoreMocks: true`](/config/restoremocks), these mocks leak between tests and cause confusing failures. The easiest fix is enabling that config option globally. + +On a related note, AI tools tend to mock modules using string paths (`vi.mock('./module.js')`) when the `import()` form (`vi.mock(import('./module.js'))`) is preferable for type safety and automatic refactoring. See [Mock Functions](/guide/learn/mock-functions#mocking-modules) for why this matters. + +### Verbose Test Names + +AI tends to produce names like "should correctly return the formatted price string when given a valid positive number and a supported currency code." These are hard to scan when you have dozens of tests. Shorter names that describe the behavior work better: "formats USD prices", "throws for negative amounts", "returns empty array when no items match." + +### Watch Mode + +Vitest runs in watch mode by default, waiting for file changes and re-running tests interactively. Vitest tries to detect CI and non-interactive or agent environments and disable watch mode automatically, but this detection can be fragile. + +When telling an AI agent to run tests, always use `vitest run` or `vitest --no-watch` to ensure the process exits after the tests finish. diff --git a/docs/guide/learn/writing-tests.md b/docs/guide/learn/writing-tests.md new file mode 100644 index 000000000..2400da699 --- /dev/null +++ b/docs/guide/learn/writing-tests.md @@ -0,0 +1,175 @@ +--- +title: Writing Tests | Guide +prev: + text: Getting Started + link: /guide/ +next: + text: Using Matchers + link: /guide/learn/matchers +--- + +# Writing Tests + +In the [Getting Started](/guide/) guide, you installed Vitest and ran your first test. This page dives deeper into how to write and organize tests in Vitest. + +## Your First Test + +A test verifies that a piece of code produces the expected result. In Vitest, you use the [`test`](/api/test) function to define a test, and [`expect`](/api/expect) to make assertions. Each test has a name (a string describing what it checks) and a function that contains one or more assertions. If any assertion fails, the test fails. + +```js +import { expect, test } from 'vitest' + +test('Math.sqrt works for perfect squares', () => { + expect(Math.sqrt(4)).toBe(2) + expect(Math.sqrt(144)).toBe(12) + expect(Math.sqrt(0)).toBe(0) +}) +``` + +::: details Use `test` or `it`? +You might also see tests written with [`it`](/api/test) instead of `test`. They behave identically. `it` is just an alias that some people prefer because it reads more naturally with a descriptive name: + +```js +import { expect, it } from 'vitest' + +it('should compute square roots', () => { + expect(Math.sqrt(4)).toBe(2) +}) +``` + +Use whichever you prefer. Both work the same way, and you can mix them freely in a project. If you want to enforce a consistent choice across your codebase, the [`consistent-test-it`](https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/consistent-test-it.md) ESLint rule (also available in [oxlint](https://oxc.rs/docs/guide/usage/linter/rules/jest/consistent-test-it.html)) can help with that. +::: + +## Grouping Tests with `describe` + +As your test files grow, you'll want to organize related tests together. [`describe`](/api/describe) creates a test suite, which is a named group of tests: + +```js +import { describe, expect, test } from 'vitest' + +describe('Math.sqrt', () => { + test('returns the square root of perfect squares', () => { + expect(Math.sqrt(4)).toBe(2) + expect(Math.sqrt(9)).toBe(3) + }) + + test('returns NaN for negative numbers', () => { + expect(Math.sqrt(-1)).toBeNaN() + }) + + test('returns 0 for 0', () => { + expect(Math.sqrt(0)).toBe(0) + }) +}) +``` + +You can nest `describe` blocks for further organization, but keep nesting shallow. Deeply nested tests are harder to read. A flat list of tests is often enough for simple modules, and `describe` becomes more useful when a file tests multiple functions or methods that each need their own group. + +## Test Files + +By default, Vitest looks for any file that contains `.test.` or `.spec.` in its name, such as `utils.test.js`, `app.spec.js`, or `math.test.jsx`. It searches in all subdirectories, so it doesn't matter where you place them. + +The exact patterns are: + +- `**/*.test.{ts,js,mjs,cjs,tsx,jsx}` +- `**/*.spec.{ts,js,mjs,cjs,tsx,jsx}` + +There's no single "right" way to organize your test files. Some teams prefer placing tests right next to the source code they test, while others keep them in a dedicated directory. Vitest will find them either way: + +``` +src/ + utils.js + utils.test.js # co-located with the source + __tests__/ + utils.test.js # in a test directory +``` + +If the default patterns don't work for your project, you can customize which files are included with the [`include`](/config/include) and [`exclude`](/config/exclude) config options. + +## Reading Test Output + +When you run `vitest` and only a single test file matches, the output is expanded into a tree structure showing `describe` groups and individual tests along with their duration: + +<<< ./snippets/test-output-single.ansi + +When multiple test files run, Vitest collapses each file into a single line to keep the output manageable: + +<<< ./snippets/test-output-multiple.ansi + +When a test fails, Vitest shows you exactly what went wrong. You'll see the expected value, the actual value, a diff highlighting the difference, and a code snippet of the surrounding lines with the failing assertion highlighted. It also includes the file and line number so you can jump straight to the source: + +<<< ./snippets/test-output-fail.ansi + +Between the diff and the code snippet, you can usually understand what went wrong without needing to add extra `console.log` statements or open the file yourself. + +## Skipping and Focusing Tests + +While developing, you'll often want to run only a subset of tests. Vitest provides modifiers for this: + +[`.only`](/api/test#only) tells Vitest to run only this test (or suite) and skip everything else in the file. This is useful when you're working on a specific test and don't want to wait for the entire suite to finish: + +```js +test.only('focus on this test', () => { + // only this test runs in the file +}) +``` + +[`.skip`](/api/test#skip) does the opposite. It skips a test without removing it, which is handy when a test is temporarily broken or you want to ignore it while you work on something else: + +```js +test.skip('not ready yet', () => { + // this test is skipped +}) +``` + +[`.todo`](/api/test#todo) lets you mark a placeholder for a test you haven't written yet. Vitest will list it in the output so you won't forget about it: + +```js +test.todo('implement validation later') +``` + +These modifiers are great for quick, local changes while developing. For more permanent ways to filter tests (by filename, line number, or tags), see the [Test Filtering](/guide/filtering) guide. + +## Using Global Imports + +By default, you import `test`, `expect`, `describe`, and other functions from `vitest` at the top of every test file. If you'd rather use them as globals without importing (similar to how Jest works), you can enable the [`globals`](/config/globals) option in your config: + +```js [vitest.config.js] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + }, +}) +``` + +With this enabled, you can write tests without the import line: + +```js +test('no import needed', () => { + expect(1 + 1).toBe(2) +}) +``` + +::: tip +If you use TypeScript, add `"types": ["vitest/globals"]` to your `tsconfig.json` `compilerOptions` for proper type support. +::: + +## Running Tests + +Vitest runs all test files **in parallel** by default, using [child processes](/config/pool). Each test file runs in its own isolated context, so your test files don't share state with each other. This prevents tests in different files from accidentally interfering. + +Tests **within** a single file run sequentially by default, which is usually what you want since tests in the same file often share setup code. If your tests are truly independent, you can opt into running them concurrently with [`test.concurrent`](/api/test#concurrent) to speed things up: + +```js +test.concurrent('first concurrent test', async () => { + // runs in parallel with the next test +}) + +test.concurrent('second concurrent test', async () => { + // runs in parallel with the previous test +}) +``` + +See the [Parallelism](/guide/parallelism) guide for more details on controlling test execution. diff --git a/docs/guide/lifecycle.md b/docs/guide/lifecycle.md index aa98886f9..0cac2720e 100644 --- a/docs/guide/lifecycle.md +++ b/docs/guide/lifecycle.md @@ -5,6 +5,10 @@ outline: deep # Test Run Lifecycle +::: tip +Looking for a practical introduction to `beforeEach`, `afterEach`, and other hooks? See the [Setup and Teardown](/guide/learn/setup-teardown) tutorial. +::: + Understanding the test run lifecycle is essential for writing effective tests, debugging issues, and optimizing your test suite. This guide explains when and in what order different lifecycle phases occur in Vitest, from initialization to teardown. ## Overview diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index bf4c32e10..57d2abb22 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -5,6 +5,10 @@ outline: false # Mocking +::: tip +New to mocking? Start with the [Mock Functions](/guide/learn/mock-functions) tutorial for a hands-on introduction to `vi.fn`, `vi.spyOn`, and `vi.mock`. +::: + When writing tests it's only a matter of time before you need to create a "fake" version of an internal — or external — service. This is commonly referred to as **mocking**. Vitest provides utility functions to help you out through its `vi` helper. You can import it from `vitest` or access it globally if [`global` configuration](/config/globals) is enabled. ::: warning diff --git a/docs/guide/snapshot.md b/docs/guide/snapshot.md index f9156e0e0..91ed8628d 100644 --- a/docs/guide/snapshot.md +++ b/docs/guide/snapshot.md @@ -4,6 +4,10 @@ title: Snapshot | Guide # Snapshot +::: tip +For a beginner-friendly introduction to snapshot testing, see the [Snapshot Testing](/guide/learn/snapshots) tutorial. +::: + Learn Snapshot by video from Vue School Snapshot tests are a very useful tool whenever you want to make sure the output of your functions does not change unexpectedly. diff --git a/docs/guide/why.md b/docs/guide/why.md index 79d876b93..14c2b7f45 100644 --- a/docs/guide/why.md +++ b/docs/guide/why.md @@ -5,7 +5,7 @@ title: Why Vitest | Guide # Why Vitest :::tip NOTE -This guide assumes that you are familiar with Vite. A good way to start learning more is to read the [Why Vite Guide](https://vitejs.dev/guide/why.html), and [Next generation frontend tooling with ViteJS](https://www.youtube.com/watch?v=UJypSr8IhKY), a stream where [Evan You](https://bsky.app/profile/evanyou.me) did a demo explaining the main concepts. +Vitest is powered by Vite. While you do not need to know Vite to use Vitest, understanding Vite helps explain some of Vitest's unique advantages. To learn more about Vite, read the [Why Vite Guide](https://vitejs.dev/guide/why.html) or watch [Next generation frontend tooling with ViteJS](https://www.youtube.com/watch?v=UJypSr8IhKY) by [Evan You](https://bsky.app/profile/evanyou.me). ::: ## The Need for a Vite Native Test Runner