Skip to content

Commit

Permalink
fix(runner): the fixture of test.extend should be init once time in…
Browse files Browse the repository at this point in the history
… all test (#4168)
  • Loading branch information
Dunqing authored Sep 27, 2023
1 parent 4e94120 commit a5979ea
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 40 deletions.
72 changes: 59 additions & 13 deletions packages/runner/src/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,33 @@ export function mergeContextFixtures(fixtures: Record<string, any>, context: { f
return context
}

const fixtureValueMap = new Map<FixtureItem, any>()
const fixtureCleanupFnMap = new Map<string, Array<() => void | Promise<void>>>()

export async function callFixtureCleanup(id: string) {
const cleanupFnArray = fixtureCleanupFnMap.get(id)
if (!cleanupFnArray)
return

for (const cleanup of cleanupFnArray.reverse())
await cleanup()

fixtureCleanupFnMap.delete(id)
}

export function withFixtures(fn: Function, testContext?: TestContext) {
return (hookContext?: TestContext) => {
const context: TestContext & { [key: string]: any } | undefined = hookContext || testContext

if (!context)
return fn({})

let cleanupFnArray = fixtureCleanupFnMap.get(context.task.suite.id)!
if (!cleanupFnArray) {
cleanupFnArray = []
fixtureCleanupFnMap.set(context.task.suite.id, cleanupFnArray)
}

const fixtures = getFixture(context)
if (!fixtures?.length)
return fn(context)
Expand All @@ -63,21 +83,47 @@ export function withFixtures(fn: Function, testContext?: TestContext) {
const pendingFixtures = resolveDeps(usedFixtures)
let cursor = 0

async function use(fixtureValue: any) {
const { prop } = pendingFixtures[cursor++]
context![prop] = fixtureValue

if (cursor < pendingFixtures.length)
await next()
else await fn(context)
}
return new Promise((resolve, reject) => {
async function use(fixtureValue: any) {
const fixture = pendingFixtures[cursor++]
context![fixture.prop] = fixtureValue

if (!fixtureValueMap.has(fixture)) {
fixtureValueMap.set(fixture, fixtureValue)
cleanupFnArray.unshift(() => {
fixtureValueMap.delete(fixture)
})
}

if (cursor < pendingFixtures.length) {
await next()
}
else {
// When all fixtures setup, call the test function
try {
resolve(await fn(context))
}
catch (err) {
reject(err)
}
return new Promise<void>((resolve) => {
cleanupFnArray.push(resolve)
})
}
}

async function next() {
const { value } = pendingFixtures[cursor]
typeof value === 'function' ? await value(context, use) : await use(value)
}
async function next() {
const fixture = pendingFixtures[cursor]
const { isFn, value } = fixture
if (fixtureValueMap.has(fixture))
return use(fixtureValueMap.get(fixture))
else
return isFn ? value(context, use) : use(value)
}

return next()
const setupFixturePromise = next()
cleanupFnArray.unshift(() => setupFixturePromise)
})
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/runner/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { collectTests } from './collect'
import { setCurrentTest } from './test-state'
import { hasFailed, hasTests } from './utils/tasks'
import { PendingError } from './errors'
import { callFixtureCleanup } from './fixture'

const now = Date.now

Expand Down Expand Up @@ -321,6 +322,7 @@ export async function runSuite(suite: Suite, runner: VitestRunner) {
}

try {
await callFixtureCleanup(suite.id)
await callSuiteHook(suite, suite, 'afterAll', runner, [suite])
await callCleanupHooks(beforeAllCleanups)
}
Expand Down
181 changes: 154 additions & 27 deletions test/core/test/test-extend.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable prefer-rest-params */
/* eslint-disable no-empty-pattern */
import { describe, expect, expectTypeOf, test, vi } from 'vitest'
import { afterAll, afterEach, beforeEach, describe, expect, expectTypeOf, test, vi } from 'vitest'

interface Fixtures {
todoList: number[]
Expand Down Expand Up @@ -38,39 +38,34 @@ const myTest = test
})

describe('test.extend()', () => {
myTest('todoList and doneList', ({ todoList, doneList, archiveList }) => {
expect(todoFn).toBeCalledTimes(1)
expect(doneFn).toBeCalledTimes(1)

expectTypeOf(todoList).toEqualTypeOf<number[]>()
expectTypeOf(doneList).toEqualTypeOf<number[]>()
expectTypeOf(doneList).toEqualTypeOf<number[]>()
describe('basic', () => {
myTest('todoList and doneList', ({ todoList, doneList, archiveList }) => {
expect(todoFn).toBeCalledTimes(1)
expect(doneFn).toBeCalledTimes(1)

expect(todoList).toEqual([1, 2, 3])
expect(doneList).toEqual([])
expect(archiveList).toEqual([])
expectTypeOf(todoList).toEqualTypeOf<number[]>()
expectTypeOf(doneList).toEqualTypeOf<number[]>()
expectTypeOf(doneList).toEqualTypeOf<number[]>()

doneList.push(todoList.shift()!)
expect(todoList).toEqual([2, 3])
expect(doneList).toEqual([1])
expect(todoList).toEqual([1, 2, 3])
expect(doneList).toEqual([])
expect(archiveList).toEqual([])

doneList.push(todoList.shift()!)
expect(todoList).toEqual([3])
expect(doneList).toEqual([1, 2])
doneList.push(todoList.shift()!)
expect(todoList).toEqual([2, 3])
expect(doneList).toEqual([1])

archiveList.push(todoList.shift()!)
expect(todoList).toEqual([])
expect(archiveList).toEqual([3])
doneList.push(todoList.shift()!)
expect(todoList).toEqual([3])
expect(doneList).toEqual([1, 2])

archiveList.pop()
})
archiveList.push(todoList.shift()!)
expect(todoList).toEqual([])
expect(archiveList).toEqual([3])

myTest('should called cleanup functions', ({ todoList, doneList, archiveList }) => {
expect(todoList).toEqual([1, 2, 3])
expect(doneList).toEqual([])
expect(archiveList).toEqual([])
archiveList.pop()
})
})

describe('smartly init fixtures', () => {
myTest('should not init any fixtures', function () {
expect(todoFn).not.toBeCalled()
Expand Down Expand Up @@ -150,4 +145,136 @@ describe('test.extend()', () => {
expect(archive).toEqual([])
})
})

describe('fixture call times', () => {
const apiFn = vi.fn(() => true)
const serviceFn = vi.fn(() => true)
const teardownFn = vi.fn()

interface APIFixture {
api: boolean
service: boolean
}

const testAPI = test.extend<APIFixture>({
api: async ({}, use) => {
await use(apiFn())
apiFn.mockClear()
teardownFn()
},
service: async ({}, use) => {
await use(serviceFn())
serviceFn.mockClear()
teardownFn()
},
})

beforeEach<APIFixture>(({ api, service }) => {
expect(api).toBe(true)
expect(service).toBe(true)
})

testAPI('Should init1 time', ({ api }) => {
expect(api).toBe(true)
expect(apiFn).toBeCalledTimes(1)
})

testAPI('Should init 1 time has multiple fixture', ({ api, service }) => {
expect(api).toBe(true)
expect(service).toBe(true)
expect(serviceFn).toBeCalledTimes(1)
expect(apiFn).toBeCalledTimes(1)
})

afterEach<APIFixture>(({ api, service }) => {
expect(api).toBe(true)
expect(service).toBe(true)
expect(apiFn).toBeCalledTimes(1)
expect(serviceFn).toBeCalledTimes(1)
})

afterAll(() => {
expect(serviceFn).toBeCalledTimes(0)
expect(apiFn).toBeCalledTimes(0)
expect(teardownFn).toBeCalledTimes(2)
})
})

describe('fixture in nested describe', () => {
interface Fixture {
foo: number
bar: number
}

const fooFn = vi.fn(() => 0)
const fooCleanup = vi.fn()

const barFn = vi.fn(() => 0)
const barCleanup = vi.fn()

const nestedTest = test.extend<Fixture>({
async foo({}, use) {
await use(fooFn())
fooCleanup()
},
async bar({}, use) {
await use(barFn())
barCleanup()
},
})

beforeEach<Fixture>(({ foo }) => {
expect(foo).toBe(0)
})

nestedTest('should only initialize foo', ({ foo }) => {
expect(foo).toBe(0)
expect(fooFn).toBeCalledTimes(1)
expect(barFn).toBeCalledTimes(0)
})

describe('level 2, using both foo and bar together', () => {
beforeEach<Fixture>(({ foo, bar }) => {
expect(foo).toBe(0)
expect(bar).toBe(0)
})

nestedTest('should only initialize bar', ({ foo, bar }) => {
expect(foo).toBe(0)
expect(bar).toBe(0)
expect(fooFn).toBeCalledTimes(1)
expect(barFn).toBeCalledTimes(1)
})

afterEach<Fixture>(({ foo, bar }) => {
expect(foo).toBe(0)
expect(bar).toBe(0)
})

afterAll(() => {
// foo setup in outside describe
// cleanup also called in outside describe
expect(fooCleanup).toHaveBeenCalledTimes(0)
// bar setup in inside describe
// cleanup also called in inside describe
expect(barCleanup).toHaveBeenCalledTimes(1)
})
})

nestedTest('level 2 will not call foo cleanup', ({ foo }) => {
expect(foo).toBe(0)
expect(fooFn).toBeCalledTimes(1)
})

afterEach<Fixture>(({ foo }) => {
expect(foo).toBe(0)
})

afterAll(() => {
// foo setup in this describe
// cleanup also called in this describe
expect(fooCleanup).toHaveBeenCalledTimes(1)
expect(barCleanup).toHaveBeenCalledTimes(1)
})
})
})

0 comments on commit a5979ea

Please sign in to comment.