--- name: test-context-fixtures description: Test context, custom fixtures with test.extend --- # Test Context & Fixtures ## Built-in Context Every test receives context as first argument: ```ts test('context', ({ task, expect, skip }) => { console.log(task.name); // Test name expect(1).toBe(1); // Context-bound expect skip(); // Skip test dynamically }); ``` ### Context Properties - `task` - Test metadata (name, file, etc.) - `expect` - Expect bound to this test (important for concurrent tests) - `skip(condition?, message?)` - Skip the test - `onTestFinished(fn)` - Cleanup after test - `onTestFailed(fn)` - Run on failure only ## Custom Fixtures with test.extend Create reusable test utilities: ```ts import { test as base } from 'vitest'; // Define fixture types interface Fixtures { db: Database; user: User; } // Create extended test export const test = base.extend({ // Fixture with setup/teardown db: async ({}, use) => { const db = await createDatabase(); await use(db); // Provide to test await db.close(); // Cleanup }, // Fixture depending on another fixture user: async ({ db }, use) => { const user = await db.createUser({ name: 'Test' }); await use(user); await db.deleteUser(user.id); }, }); ``` Using fixtures: ```ts test('query user', async ({ db, user }) => { const found = await db.findUser(user.id); expect(found).toEqual(user); }); ``` ## Fixture Initialization Fixtures only initialize when accessed: ```ts const test = base.extend({ expensive: async ({}, use) => { console.log('initializing'); // Only runs if test uses it await use('value'); }, }); test('no fixture', () => {}); // expensive not called test('uses fixture', ({ expensive }) => {}); // expensive called ``` ## Auto Fixtures Run fixture for every test: ```ts const test = base.extend({ setup: [ async ({}, use) => { await globalSetup(); await use(); await globalTeardown(); }, { auto: true }, // Always run ], }); ``` ## Scoped Fixtures ### File Scope Initialize once per file: ```ts const test = base.extend({ connection: [ async ({}, use) => { const conn = await connect(); await use(conn); await conn.close(); }, { scope: 'file' }, ], }); ``` ### Worker Scope Initialize once per worker: ```ts const test = base.extend({ sharedResource: [ async ({}, use) => { await use(globalResource); }, { scope: 'worker' }, ], }); ``` ## Injected Fixtures (from Config) Override fixtures per project: ```ts // test file const test = base.extend({ apiUrl: ['/default', { injected: true }], }); // vitest.config.ts defineConfig({ test: { projects: [ { test: { name: 'prod', provide: { apiUrl: 'https://api.prod.com' }, }, }, ], }, }); ``` ## Scoped Values per Suite Override fixture for specific suite: ```ts const test = base.extend({ environment: 'development', }); describe('production tests', () => { test.scoped({ environment: 'production' }); test('uses production', ({ environment }) => { expect(environment).toBe('production'); }); }); test('uses default', ({ environment }) => { expect(environment).toBe('development'); }); ``` ## Extended Test Hooks Type-aware hooks with fixtures: ```ts const test = base.extend<{ db: Database }>({ db: async ({}, use) => { const db = await createDb(); await use(db); await db.close(); }, }); // Hooks know about fixtures test.beforeEach(({ db }) => { db.seed(); }); test.afterEach(({ db }) => { db.clear(); }); ``` ## Composing Fixtures Extend from another extended test: ```ts // base-test.ts export const test = base.extend<{ db: Database }>({ db: async ({}, use) => { /* ... */ }, }); // admin-test.ts import { test as dbTest } from './base-test'; export const test = dbTest.extend<{ admin: User }>({ admin: async ({ db }, use) => { const admin = await db.createAdmin(); await use(admin); }, }); ``` ## Key Points - Use `{ }` destructuring to access fixtures - Fixtures are lazy - only initialize when accessed - Return cleanup function from fixtures - Use `{ auto: true }` for setup fixtures - Use `{ scope: 'file' }` for expensive shared resources - Fixtures compose - extend from extended tests