Skip to content

Commit

Permalink
feat(runner): support automatic fixtures (#5102)
Browse files Browse the repository at this point in the history
Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>
  • Loading branch information
fenghan34 and sheremet-va committed Feb 16, 2024
1 parent bc5b2d0 commit 0441f76
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 14 deletions.
26 changes: 26 additions & 0 deletions docs/guide/test-context.md
Expand Up @@ -161,6 +161,32 @@ myTest('', ({ todos }) => {})
When using `test.extend()` with fixtures, you should always use the object destructuring pattern `{ todos }` to access context both in fixture function and test function.
:::

#### Automatic fixture

::: warning
This feature is available since Vitest 1.3.0.
:::

Vitest also supports the tuple syntax for fixtures, allowing you to pass options for each fixture. For example, you can use it to explicitly initialize a fixture, even if it's not being used in tests.

```ts
import { test as base } from 'vitest'

const test = base.extend({
fixture: [
async ({}, use) => {
// this function will run
setup()
await use()
teardown()
},
{ auto: true } // Mark as an automatic fixture
],
})

test('', () => {})
```

#### TypeScript

To provide fixture types for all your custom contexts, you can pass the fixtures type as a generic.
Expand Down
35 changes: 22 additions & 13 deletions packages/runner/src/fixture.ts
@@ -1,11 +1,10 @@
import { createDefer } from '@vitest/utils'
import { createDefer, isObject } from '@vitest/utils'
import { getFixture } from './map'
import type { TestContext } from './types'
import type { FixtureOptions, TestContext } from './types'

export interface FixtureItem {
export interface FixtureItem extends FixtureOptions {
prop: string
value: any
index: number
/**
* Indicates whether the fixture is a function
*/
Expand All @@ -17,15 +16,24 @@ export interface FixtureItem {
}

export function mergeContextFixtures(fixtures: Record<string, any>, context: { fixtures?: FixtureItem[] } = {}) {
const fixtureOptionKeys = ['auto']
const fixtureArray: FixtureItem[] = Object.entries(fixtures)
.map(([prop, value], index) => {
const isFn = typeof value === 'function'
return {
prop,
value,
index,
isFn,
.map(([prop, value]) => {
const fixtureItem = { value } as FixtureItem

if (
Array.isArray(value) && value.length >= 2
&& isObject(value[1])
&& Object.keys(value[1]).some(key => fixtureOptionKeys.includes(key))
) {
// fixture with options
Object.assign(fixtureItem, value[1])
fixtureItem.value = value[0]
}

fixtureItem.prop = prop
fixtureItem.isFn = typeof fixtureItem.value === 'function'
return fixtureItem
})

if (Array.isArray(context.fixtures))
Expand Down Expand Up @@ -67,7 +75,8 @@ export function withFixtures(fn: Function, testContext?: TestContext) {
return fn(context)

const usedProps = getUsedProps(fn)
if (!usedProps.length)
const hasAutoFixture = fixtures.some(({ auto }) => auto)
if (!usedProps.length && !hasAutoFixture)
return fn(context)

if (!fixtureValueMaps.get(context))
Expand All @@ -78,7 +87,7 @@ export function withFixtures(fn: Function, testContext?: TestContext) {
cleanupFnArrayMap.set(context, [])
const cleanupFnArray = cleanupFnArrayMap.get(context)!

const usedFixtures = fixtures.filter(({ prop }) => usedProps.includes(prop))
const usedFixtures = fixtures.filter(({ prop, auto }) => auto || usedProps.includes(prop))
const pendingFixtures = resolveDeps(usedFixtures)

if (!pendingFixtures.length)
Expand Down
9 changes: 8 additions & 1 deletion packages/runner/src/types/tasks.ts
Expand Up @@ -223,6 +223,13 @@ export type TestAPI<ExtraContext = {}> = ChainableTestAPI<ExtraContext> & Extend
K extends keyof ExtraContext ? ExtraContext[K] : never }>
}

export interface FixtureOptions {
/**
* Whether to automatically set up current fixture, even though it's not being used in tests.
*/
auto?: boolean
}

export type Use<T> = (value: T) => Promise<void>
export type FixtureFn<T, K extends keyof T, ExtraContext> =
(context: Omit<T, K> & ExtraContext, use: Use<T[K]>) => Promise<void>
Expand All @@ -231,7 +238,7 @@ export type Fixture<T, K extends keyof T, ExtraContext = {}> =
? (T[K] extends any ? FixtureFn<T, K, Omit<ExtraContext, Exclude<keyof T, K>>> : never)
: T[K] | (T[K] extends any ? FixtureFn<T, K, Omit<ExtraContext, Exclude<keyof T, K>>> : never)
export type Fixtures<T extends Record<string, any>, ExtraContext = {}> = {
[K in keyof T]: Fixture<T, K, ExtraContext & ExtendedContext<Test>>
[K in keyof T]: Fixture<T, K, ExtraContext & ExtendedContext<Test>> | [Fixture<T, K, ExtraContext & ExtendedContext<Test>>, FixtureOptions?]
}

export type InferFixturesTypes<T> = T extends TestAPI<infer C> ? C : T
Expand Down
43 changes: 43 additions & 0 deletions test/core/test/fixture-options.test.ts
@@ -0,0 +1,43 @@
import { afterAll, beforeEach, describe, expect, test, vi } from 'vitest'

const mockServer = { setup: vi.fn(), teardown: vi.fn() }
const FnA = vi.fn()

const myTest = test.extend<{
autoFixture: void
normalFixture: any[]
}>({
autoFixture: [async ({}, use) => {
await mockServer.setup()
await use()
await mockServer.teardown()
}, { auto: true }],

normalFixture: [async () => {
await FnA()
}, {}],
})

describe('fixture with options', () => {
describe('automatic fixture', () => {
beforeEach(() => {
expect(mockServer.setup).toBeCalledTimes(1)
})

afterAll(() => {
expect(mockServer.setup).toBeCalledTimes(1)
expect(mockServer.teardown).toBeCalledTimes(1)
})

myTest('should setup mock server', () => {
expect(mockServer.setup).toBeCalledTimes(1)
})
})

describe('normal fixture', () => {
myTest('it is not a fixture with options', ({ normalFixture }) => {
expect(FnA).not.toBeCalled()
expect(normalFixture).toBeInstanceOf(Array)
})
})
})

0 comments on commit 0441f76

Please sign in to comment.