Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vitest): add onTestFinished hook #5128

Merged
merged 3 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,10 @@ afterEach(async () => {

Here, the `afterEach` ensures that testing data is cleared after each test runs.

::: tip
Vitest 1.3.0 added [`onTestFinished`](##ontestfinished-1-3-0) hook. You can call it during the test execution to cleanup any state after the test has finished running.
:::

### beforeAll

- **Type:** `beforeAll(fn: () => Awaitable<void>, timeout?: number)`
Expand Down Expand Up @@ -906,3 +910,96 @@ afterAll(async () => {
```

Here the `afterAll` ensures that `stopMocking` method is called after all tests run.

## Test Hooks

Vitest provides a few hooks that you can call _during_ the test execution to cleanup the state when the test has finished runnning.

::: warning
These hooks will throw an error if they are called outside of the test body.
:::

### onTestFinished <Badge type="info">1.3.0+</Badge>

This hook is always called after the test has finished running. It is called after `afterEach` hooks since they can influence the test result. It receives a `TaskResult` object with the current test result.

```ts
import { onTestFinished, test } from 'vitest'

test('performs a query', () => {
const db = connectDb()
onTestFinished(() => db.close())
db.query('SELECT * FROM users')
})
```

::: warning
If you are running tests concurrently, you should always use `onTestFinished` hook from the test context since Vitest doesn't track concurrent tests in global hooks:

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

test.concurrent('performs a query', (t) => {
const db = connectDb()
t.onTestFinished(() => db.close())
db.query('SELECT * FROM users')
})
```
:::

This hook is particularly useful when creating reusable logic:

```ts
// this can be in a separate file
function getTestDb() {
const db = connectMockedDb()
onTestFinished(() => db.close())
return db
}

test('performs a user query', async () => {
const db = getTestDb()
expect(
await db.query('SELECT * from users').perform()
).toEqual([])
})

test('performs an organization query', async () => {
const db = getTestDb()
expect(
await db.query('SELECT * from organizations').perform()
).toEqual([])
})
```

### onTestFailed

This hook is called only after the test has failed. It is called after `afterEach` hooks since they can influence the test result. It receives a `TaskResult` object with the current test result. This hook is useful for debugging.

```ts
import { onTestFailed, test } from 'vitest'

test('performs a query', () => {
const db = connectDb()
onTestFailed((e) => {
console.log(e.result.errors)
})
db.query('SELECT * FROM users')
})
```

::: warning
If you are running tests concurrently, you should always use `onTestFailed` hook from the test context since Vitest doesn't track concurrent tests in global hooks:

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

test.concurrent('performs a query', (t) => {
const db = connectDb()
onTestFailed((result) => {
console.log(result.errors)
})
db.query('SELECT * FROM users')
})
```
:::
5 changes: 5 additions & 0 deletions packages/runner/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export function createTestContext<T extends Test | Custom>(test: T, runner: Vite
test.onFailed.push(fn)
}

context.onTestFinished = (fn) => {
test.onFinished ||= []
test.onFinished.push(fn)
}

return runner.extendTaskContext?.(context) as ExtendedContext<T> || context
}

Expand Down
9 changes: 7 additions & 2 deletions packages/runner/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { OnTestFailedHandler, SuiteHooks, TaskPopulated } from './types'
import type { OnTestFailedHandler, OnTestFinishedHandler, SuiteHooks, TaskPopulated } from './types'
import { getCurrentSuite, getRunner } from './suite'
import { getCurrentTest } from './test-state'
import { withTimeout } from './context'
Expand Down Expand Up @@ -27,13 +27,18 @@ export const onTestFailed = createTestHook<OnTestFailedHandler>('onTestFailed',
test.onFailed.push(handler)
})

export const onTestFinished = createTestHook<OnTestFinishedHandler>('onTestFinished', (test, handler) => {
test.onFinished ||= []
test.onFinished.push(handler)
})

function createTestHook<T>(name: string, handler: (test: TaskPopulated, handler: T) => void) {
return (fn: T) => {
const current = getCurrentTest()

if (!current)
throw new Error(`Hook ${name}() can only be called inside a test`)

handler(current, fn)
return handler(current, fn)
}
}
2 changes: 1 addition & 1 deletion packages/runner/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { startTests, updateTask } from './run'
export { test, it, describe, suite, getCurrentSuite, createTaskCollector } from './suite'
export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed } from './hooks'
export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed, onTestFinished } from './hooks'
export { setFn, getFn, getHooks, setHooks } from './map'
export { getCurrentTest } from './test-state'
export { processError } from '@vitest/utils/error'
Expand Down
17 changes: 15 additions & 2 deletions packages/runner/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,21 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) {
}
}

if (test.result.state === 'fail')
await Promise.all(test.onFailed?.map(fn => fn(test.result!)) || [])
try {
await Promise.all(test.onFinished?.map(fn => fn(test.result!)) || [])
}
catch (e) {
failTask(test.result, e, runner.config.diffOptions)
}

if (test.result.state === 'fail') {
try {
await Promise.all(test.onFailed?.map(fn => fn(test.result!)) || [])
}
catch (e) {
failTask(test.result, e, runner.config.diffOptions)
}
}

// if test is marked to be failed, flip the result
if (test.fails) {
Expand Down
7 changes: 7 additions & 0 deletions packages/runner/src/types/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface TaskPopulated extends TaskBase {
result?: TaskResult
fails?: boolean
onFailed?: OnTestFailedHandler[]
onFinished?: OnTestFinishedHandler[]
/**
* Store promises (from async expects) to wait for them before finishing the test
*/
Expand Down Expand Up @@ -296,6 +297,11 @@ export interface TaskContext<Task extends Custom | Test = Custom | Test> {
*/
onTestFailed: (fn: OnTestFailedHandler) => void

/**
* Extract hooks on test failed
*/
onTestFinished: (fn: OnTestFinishedHandler) => void

/**
* Mark tests as skipped. All execution after this call will be skipped.
*/
Expand All @@ -305,6 +311,7 @@ export interface TaskContext<Task extends Custom | Test = Custom | Test> {
export type ExtendedContext<T extends Custom | Test> = TaskContext<T> & TestContext

export type OnTestFailedHandler = (result: TaskResult) => Awaitable<void>
export type OnTestFinishedHandler = (result: TaskResult) => Awaitable<void>

export type SequenceHooks = 'stack' | 'list' | 'parallel'
export type SequenceSetupFiles = 'list' | 'parallel'
1 change: 1 addition & 0 deletions packages/vitest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {
afterAll,
afterEach,
onTestFailed,
onTestFinished,
} from '@vitest/runner'
export { bench } from './runtime/benchmark'

Expand Down
43 changes: 43 additions & 0 deletions test/core/test/on-finished.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect, it, onTestFinished } from 'vitest'

const collected: any[] = []

it('on-finished regular', () => {
collected.push(1)
onTestFinished(() => {
collected.push(3)
})
collected.push(2)
})

it('on-finished context', (t) => {
collected.push(4)
t.onTestFinished(() => {
collected.push(6)
})
collected.push(5)
})

it.fails('failed finish', () => {
collected.push(7)
onTestFinished(() => {
collected.push(9)
})
collected.push(8)
expect.fail('failed')
collected.push(null)
})

it.fails('failed finish context', (t) => {
collected.push(10)
t.onTestFinished(() => {
collected.push(12)
})
collected.push(11)
expect.fail('failed')
collected.push(null)
})

it('after', () => {
expect(collected).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
})
Loading