docs/guide/lifecycle.md
::: tip
Looking for a practical introduction to beforeEach, afterEach, and other hooks? See the Setup and 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.
A typical Vitest test run goes through these main phases:
Phases 4–6 run once for each test file, so across your test suite they will execute multiple times and may also run in parallel across different files when you use more than 1 worker.
When you run vitest, the framework first loads your configuration and prepares the test environment.
What happens:
This phase can run again if the config file or one of its imports changes.
Scope: Main process (before any test workers are created)
If you have configured globalSetup files, they run once before any test workers are created.
What happens:
setup() functions (or exported default function) from global setup files execute sequentiallyScope: Main process (separate from test workers)
Important notes:
provide/inject instead)export function setup(project) {
// Runs once before all tests
console.log('Global setup')
// Share data with tests
project.provide('apiUrl', 'http://localhost:3000')
}
export function teardown() {
// Runs once after all tests
console.log('Global teardown')
}
After global setup completes, Vitest creates test workers based on your pool configuration.
What happens:
browser.enabled or pool setting (threads, forks, vmThreads, or vmForks)Scope: Worker processes/threads
Before each test file runs, setup files are executed.
What happens:
sequence.setupFiles)Scope: Worker process (same as your tests)
Important notes:
import { afterEach } from 'vitest'
// Runs before each test file
console.log('Setup file executing')
// Register hooks that apply to all tests
afterEach(() => {
cleanup()
})
This is the main phase where your tests actually run.
Test files are executed based on your configuration:
maxWorkerssequence.shuffle or fine-tuned with sequence.sequencerThe execution follows this order:
describe blocks runs immediatelydescribe blocks are processed, and tests are registered as side effects of importing the test filearoundAll hooks: Wrap around all tests in the suite (must call runSuite())beforeAll hooks: Run once before any tests in the suitearoundEach hooks wrap around the test (must call runTest())beforeEach hooks execute (in order defined, or based on sequence.hooks)afterEach hooks execute (reverse order by default with sequence.hooks: 'stack')onTestFinished callbacks run (always in reverse order)onTestFailed callbacks runrepeats or retry are set, all of these steps are executed againafterAll hooks: Run once after all tests in the suite completeExample execution flow:
// This runs immediately (collection phase)
console.log('File loaded')
describe('User API', () => {
// This runs immediately (collection phase)
console.log('Suite defined')
aroundAll(async (runSuite) => {
// Wraps around all tests in this suite
console.log('aroundAll before')
await runSuite()
console.log('aroundAll after')
})
beforeAll(() => {
// Runs once before all tests in this suite
console.log('beforeAll')
})
aroundEach(async (runTest) => {
// Wraps around each test
console.log('aroundEach before')
await runTest()
console.log('aroundEach after')
})
beforeEach(() => {
// Runs before each test
console.log('beforeEach')
})
test('creates user', () => {
// Test executes
console.log('test 1')
})
test('updates user', () => {
// Test executes
console.log('test 2')
})
afterEach(() => {
// Runs after each test
console.log('afterEach')
})
afterAll(() => {
// Runs once after all tests in this suite
console.log('afterAll')
})
})
// Output:
// File loaded
// Suite defined
// aroundAll before
// beforeAll
// aroundEach before
// beforeEach
// test 1
// afterEach
// aroundEach after
// aroundEach before
// beforeEach
// test 2
// afterEach
// aroundEach after
// afterAll
// aroundAll after
When using nested describe blocks, hooks follow a hierarchical pattern. The aroundAll and aroundEach hooks wrap around their respective scopes, with parent hooks wrapping child hooks:
describe('outer', () => {
aroundAll(async (runSuite) => {
console.log('outer aroundAll before')
await runSuite()
console.log('outer aroundAll after')
})
beforeAll(() => console.log('outer beforeAll'))
aroundEach(async (runTest) => {
console.log('outer aroundEach before')
await runTest()
console.log('outer aroundEach after')
})
beforeEach(() => console.log('outer beforeEach'))
test('outer test', () => console.log('outer test'))
describe('inner', () => {
aroundAll(async (runSuite) => {
console.log('inner aroundAll before')
await runSuite()
console.log('inner aroundAll after')
})
beforeAll(() => console.log('inner beforeAll'))
aroundEach(async (runTest) => {
console.log('inner aroundEach before')
await runTest()
console.log('inner aroundEach after')
})
beforeEach(() => console.log('inner beforeEach'))
test('inner test', () => console.log('inner test'))
afterEach(() => console.log('inner afterEach'))
afterAll(() => console.log('inner afterAll'))
})
afterEach(() => console.log('outer afterEach'))
afterAll(() => console.log('outer afterAll'))
})
// Output:
// outer aroundAll before
// outer beforeAll
// outer aroundEach before
// outer beforeEach
// outer test
// outer afterEach
// outer aroundEach after
// inner aroundAll before
// inner beforeAll
// outer aroundEach before
// inner aroundEach before
// outer beforeEach
// inner beforeEach
// inner test
// inner afterEach
// outer afterEach
// inner aroundEach after
// outer aroundEach after
// inner afterAll
// inner aroundAll after
// outer afterAll
// outer aroundAll after
When using test.concurrent or sequence.concurrent:
beforeEach and afterEach hookstest.concurrent('name', async ({ expect }) => {})Throughout the test run, reporters receive lifecycle events and display results.
What happens:
For detailed information about the reporter lifecycle, see the Reporters guide.
After all tests complete, global teardown functions execute.
What happens:
teardown() functions from globalSetup files runScope: Main process
export function teardown() {
// Clean up global resources
console.log('Global teardown complete')
}
Understanding where code executes is crucial for avoiding common pitfalls:
| Phase | Scope | Access to Test Context | Runs |
|---|---|---|---|
| Config File | Main process | ❌ No | Once per Vitest run |
| Global Setup | Main process | ❌ No (use provide/inject) | Once per Vitest run |
| Setup Files | Worker (same as tests) | ✅ Yes | Before each test file |
| File-level code | Worker | ✅ Yes | Once per test file |
aroundAll | Worker | ✅ Yes | Once per suite (wraps all tests) |
beforeAll / afterAll | Worker | ✅ Yes | Once per suite |
aroundEach | Worker | ✅ Yes | Per test (wraps each test) |
beforeEach / afterEach | Worker | ✅ Yes | Per test |
| Test function | Worker | ✅ Yes | Once (or more with retries/repeats) |
| Global Teardown | Main process | ❌ No | Once per Vitest run |
In watch mode, the lifecycle repeats with some differences:
project.onTestsRerun for rerun-specific logic)Understanding the lifecycle helps optimize test performance:
beforeAll is better than beforeEach for expensive setup that doesn't need isolationFor tips on how to improve performance, read the Improving Performance guide.