In-source testing with import.meta.vitest
Vitest allows writing tests within source code alongside implementations using an `if (import.meta.vitest)` block. This makes tests share the same closure as implementations and able to test private states without exporting them. The feature provides a closer feedback loop for development.
Setting up in-source tests with includeSource config
To enable in-source testing, set the `includeSource` config option in vitest.config.ts with a glob pattern. For example: `includeSource: ['src/**/*.{js,ts}']` to grab files under src/.
In-source test example with add function
Example of in-source testing:
```ts
export function add(...args: number[]) {
return args.reduce((a, b) => a + b, 0)
}
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('add', () => {
expect(add()).toBe(0)
expect(add(1)).toBe(1)
expect(add(1, 2, 3)).toBe(6)
})
}
```
This shows how to write test suites inside a source file using the import.meta.vitest API.
Dead code elimination for production builds with define
For production builds, set `define: { 'import.meta.vitest': 'undefined' }` in your build config (vite.config.ts, rolldown.config.js, rollup.config.js, build.config.js, or webpack.config.js). This allows the bundler to perform dead code elimination and remove test code from production builds.
Vite define config for in-source tests
In vite.config.ts, add the define option:
```ts
export default defineConfig({
test: {
includeSource: ['src/**/*.{js,ts}'],
},
define: {
'import.meta.vitest': 'undefined',
},
})
```
TypeScript support for import.meta.vitest
To get TypeScript support for `import.meta.vitest`, add `vitest/importMeta` to the `types` array in tsconfig.json compilerOptions.
Rolldown define config for in-source tests
In rolldown.config.js, configure:
```js
export default defineConfig({
transform: {
define: {
'import.meta.vitest': 'undefined',
},
},
})
```
Rollup plugin replace for in-source tests
In rollup.config.js, use the @rollup/plugin-replace:
```js
import replace from '@rollup/plugin-replace'
export default {
plugins: [
replace({
'import.meta.vitest': 'undefined',
})
],
}
```
unbuild replace config for in-source tests
In build.config.js, configure:
```js
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
replace: {
'import.meta.vitest': 'undefined',
},
})
```
webpack DefinePlugin for in-source tests
In webpack.config.js, use DefinePlugin:
```js
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.DefinePlugin({
'import.meta.vitest': 'undefined',
})
],
}
```
Use cases for in-source testing
In-source testing is recommended for unit testing small-scoped functions or utilities, prototyping, and inline assertions. For more complex tests like components or E2E testing, it is recommended to use separate test files instead.
Limitation with assertion functions in in-source tests
There is a limitation when using assertion functions such as `assert` in in-source tests. See the assert API documentation for details and workarounds.
Describe desired test structure to AI
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'. This saves you from restructuring the output afterwards.
Constrain AI behavior with negative instructions
Tell the AI what not to do in prompts. For example: 'Test against the real implementation, don't mock any modules' or 'don't use snapshot tests'. This prevents common defaults you don't want, since AI tools tend to over-mock.
AI test generation requires context about implementation
To get good test code from AI tools, provide the source file itself with the actual implementation, not just a description. Include the full file or at least the function to be tested along with its imports and types.
Share existing test files for AI pattern matching
Share existing test files from the same project so AI can match 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.
Include Vitest config when asking AI for tests
Share your Vitest config when asking AI to generate tests, especially if you have enabled globals, set a custom environment, or configured setupFiles. Without this context, the AI might generate unnecessary imports, use the wrong test environment, or miss required setup.
Include AGENTS.md for coding conventions
If your project has an AGENTS.md or similar file with coding conventions, include that in context for AI. Many AI tools pick up on these automatically and will follow the rules defined there.
Write specific prompts for better AI test generation
Specific prompts produce better tests than generic ones. Instead of 'Write tests for userService.js', use 'Write tests for the createUser function. Cover validation errors (missing name, invalid email format, duplicate email), the successful creation path, and verify that the password is hashed before being stored.'
Ask AI for edge cases explicitly
To get comprehensive test coverage from AI, ask for edge cases explicitly. Prompts like 'Include tests for empty inputs, boundary values, and error handling' produce more thorough coverage than leaving it to the AI's judgment.
Mention Vitest features in prompts to AI
Mention specific Vitest features if you want them used, such as 'Use toMatchInlineSnapshot for the error messages' or 'use test.for for the different currency formats'. This guides the AI toward the right tools instead of letting it fall back to repetitive copy-paste tests.
Tell AI about async code explicitly
When asking AI to generate tests for async code, explicitly state it: 'The function returns a Promise' or 'this calls an external API'. This helps the AI use async/await and appropriate matchers like .resolves and .rejects.
Reference existing tests when asking for additions
When asking AI to add tests, reference existing tests: 'Follow the same style as the tests in auth.test.js'. This is more effective than describing the style from scratch, as the AI will pick up naming conventions, assertion patterns, and import styles.
Iterate on AI-generated tests through conversation
If the first AI result is not right, iterate through conversation. For example: 'These tests are too focused on implementation details. Rewrite them to only assert on the return values and thrown errors.' Refining through conversation often produces better results than trying to write the perfect prompt upfront.
Check that AI-generated tests assert meaningful behavior
Review AI-generated tests to ensure they 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 should verify actual properties and behavior, not just that a value is defined.
Always run AI-generated tests before committing
Always run AI-generated tests immediately before committing. 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 when executed. Use: vitest run src/userService.test.js
AI-generated tests often skip hard edge cases
AI tools tend to generate happy-path tests and skip the hard cases. After reviewing generated tests, ask: what happens with empty input, 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.
Workflow for AI-generated tests
Treat AI-generated tests as a first draft, not finished product. The workflow is: (1) Generate initial tests with specific prompt and good context, (2) Run them immediately to catch errors, (3) Review each test for issues, (4) Ask for revisions if entire sections need improvement, (5) Edit manually for small fixes rather than re-prompting for every detail.
AI may generate unnecessary imports with globals enabled
If your config has globals: true, AI might still add import { test, expect } from 'vitest' (harmless but unnecessary). Conversely, it might generate tests without imports when globals aren't enabled. Include your config in context to prevent this.
AI generates verbose test names
AI tends to produce long test 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. Shorter names work better: 'formats USD prices', 'throws for negative amounts', 'returns empty array when no items match.'
Test Tags overview and purpose
Tags let you label tests so you can filter what runs and override their options when needed. Tags become useful once a suite has groups of tests that share runner options, like longer timeouts for database queries or retries for integration tests on CI. Tags capture categories where a tag definition holds shared options and any test marked with the tag inherits them.
Tag expressions with boolean operators
Tag names can be combined into expressions using these keywords: 'and' or '&&' to include both expressions, 'or' or '||' to include at least one expression, 'not' or '!' to exclude an expression, '*' to match any number of characters (0 or more), and '()' to group expressions and override precedence. The parser follows standard operator precedence: not/! has highest priority, then and/&&, then or/||.
Reserved tag names
Tag names cannot be 'and', 'or', or 'not' (case-insensitive) as these are reserved keywords. Tag names also cannot contain special characters: (, ), &, |, !, *, or spaces, as these are used by the expression parser.
Defining tags in configuration
Tags must be defined in the configuration file. By default, Vitest does not provide any built-in tags. If a test uses a tag that isn't defined in the config, the test runner will throw an error. This prevents unexpected behavior from mistyped tag names. You can disable this check with the 'strictTags' option. Each tag definition must include a 'name' property and may include additional options like 'timeout', 'retry', 'priority', and 'description' that will be applied to every test marked with the tag.
Tag definition example with timeout and retry
Example tag configuration:
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
tags: [
{
name: 'frontend',
description: 'Tests written for frontend.',
},
{
name: 'backend',
description: 'Tests written for backend.',
},
{
name: 'db',
description: 'Tests for database queries.',
timeout: 60_000,
},
{
name: 'flaky',
description: 'Flaky CI tests.',
retry: process.env.CI ? 3 : 0,
timeout: 30_000,
priority: 1,
},
],
},
})
```
TypeScript type augmentation for tags
If using TypeScript, you can enforce what tags are available by augmenting the TestTags type with a property containing a union of strings. Create a file like vitest.shims.ts:
```ts
import 'vitest'
declare module 'vitest' {
interface TestTags {
tags:
| 'frontend'
| 'backend'
| 'db'
| 'flaky'
}
}
```
Make sure this file is included by your tsconfig.
Tag option conflict resolution
When several tags define the same option and are applied to the same test, they are resolved by priority first (lower number wins), then by the order they appear in the test's tags array. Tags without a priority are merged first and overridden by higher-priority ones. Options defined on the test itself always win over tag options. Example: test('flaky database test', { tags: ['flaky', 'db'] }) results in timeout: 30_000 (from flaky with priority 1) and retry: 3 (from flaky), not timeout: 60_000 from db.
Applying tags to individual tests and suites
Tags can be applied using the tags option:
```ts
import { describe, test } from 'vitest'
test('renders homepage', { tags: ['frontend'] }, () => {
// ...
})
describe('API endpoints', { tags: ['backend'] }, () => {
test('returns user data', () => {
// This test inherits the "backend" tag from the parent suite
})
test('validates input', { tags: ['validation'] }, () => {
// This test has both "backend" (inherited) and "validation" tags
})
})
```
Tag inheritance from parent suites
Tags are inherited from parent suites, so all tests inside a tagged describe block will automatically have that tag. Child tests can also have additional tags of their own.
Module-level tags with @module-tag JSDoc
You can define tags for every test in a file by using JSDoc's @module-tag at the top of the file:
```ts
/**
* Auth tests
* @module-tag admin/pages/dashboard
* @module-tag acceptance
*/
test('dashboard renders items', () => {
// ...
})
```
Module-tag applies to all tests in file
A @module-tag in a JSDoc comment applies to all tests in that file, not just the test it precedes. This means if you have multiple @module-tag comments throughout a file, every test will have all of those tags. To tag individual tests differently, use the tags option in the test function instead.
Use cases for tags vs other filtering methods
Use tags to apply timeout/retry to a category of tests or mark cross-cutting categories (flaky, slow, frontend) scattered across many files. Use tags + matchesTags to conditionally run expensive setup based on what's filtered. Use -t / testNamePattern to run a subset by test name match. Use --include / --exclude to run a subset by file path. Use Test Projects to run different files with different runner settings (isolation, pool, environment). You can combine projects and tags—a test in a Sequential project can also carry a flaky tag, and Vitest applies both.
Tags filter in Vitest UI
In Vitest UI, you can start a filter with a 'tag:' prefix to filter out tests by tags using the same tags expression syntax as the --tags-filter CLI option.