Skip to content

Commit 6f5b42b

Browse files
authoredFeb 7, 2024
feat(vitest): add onTestFinished hook (#5128)
1 parent 2085131 commit 6f5b42b

File tree

8 files changed

+176
-5
lines changed

8 files changed

+176
-5
lines changed
 

‎docs/api/index.md

+97
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,10 @@ afterEach(async () => {
853853

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

856+
::: tip
857+
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.
858+
:::
859+
856860
### beforeAll
857861

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

908912
Here the `afterAll` ensures that `stopMocking` method is called after all tests run.
913+
914+
## Test Hooks
915+
916+
Vitest provides a few hooks that you can call _during_ the test execution to cleanup the state when the test has finished runnning.
917+
918+
::: warning
919+
These hooks will throw an error if they are called outside of the test body.
920+
:::
921+
922+
### onTestFinished <Badge type="info">1.3.0+</Badge>
923+
924+
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.
925+
926+
```ts
927+
import { onTestFinished, test } from 'vitest'
928+
929+
test('performs a query', () => {
930+
const db = connectDb()
931+
onTestFinished(() => db.close())
932+
db.query('SELECT * FROM users')
933+
})
934+
```
935+
936+
::: warning
937+
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:
938+
939+
```ts
940+
import { test } from 'vitest'
941+
942+
test.concurrent('performs a query', (t) => {
943+
const db = connectDb()
944+
t.onTestFinished(() => db.close())
945+
db.query('SELECT * FROM users')
946+
})
947+
```
948+
:::
949+
950+
This hook is particularly useful when creating reusable logic:
951+
952+
```ts
953+
// this can be in a separate file
954+
function getTestDb() {
955+
const db = connectMockedDb()
956+
onTestFinished(() => db.close())
957+
return db
958+
}
959+
960+
test('performs a user query', async () => {
961+
const db = getTestDb()
962+
expect(
963+
await db.query('SELECT * from users').perform()
964+
).toEqual([])
965+
})
966+
967+
test('performs an organization query', async () => {
968+
const db = getTestDb()
969+
expect(
970+
await db.query('SELECT * from organizations').perform()
971+
).toEqual([])
972+
})
973+
```
974+
975+
### onTestFailed
976+
977+
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.
978+
979+
```ts
980+
import { onTestFailed, test } from 'vitest'
981+
982+
test('performs a query', () => {
983+
const db = connectDb()
984+
onTestFailed((e) => {
985+
console.log(e.result.errors)
986+
})
987+
db.query('SELECT * FROM users')
988+
})
989+
```
990+
991+
::: warning
992+
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:
993+
994+
```ts
995+
import { test } from 'vitest'
996+
997+
test.concurrent('performs a query', (t) => {
998+
const db = connectDb()
999+
onTestFailed((result) => {
1000+
console.log(result.errors)
1001+
})
1002+
db.query('SELECT * FROM users')
1003+
})
1004+
```
1005+
:::

‎packages/runner/src/context.ts

+5
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export function createTestContext<T extends Test | Custom>(test: T, runner: Vite
5959
test.onFailed.push(fn)
6060
}
6161

62+
context.onTestFinished = (fn) => {
63+
test.onFinished ||= []
64+
test.onFinished.push(fn)
65+
}
66+
6267
return runner.extendTaskContext?.(context) as ExtendedContext<T> || context
6368
}
6469

‎packages/runner/src/hooks.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { OnTestFailedHandler, SuiteHooks, TaskPopulated } from './types'
1+
import type { OnTestFailedHandler, OnTestFinishedHandler, SuiteHooks, TaskPopulated } from './types'
22
import { getCurrentSuite, getRunner } from './suite'
33
import { getCurrentTest } from './test-state'
44
import { withTimeout } from './context'
@@ -27,13 +27,18 @@ export const onTestFailed = createTestHook<OnTestFailedHandler>('onTestFailed',
2727
test.onFailed.push(handler)
2828
})
2929

30+
export const onTestFinished = createTestHook<OnTestFinishedHandler>('onTestFinished', (test, handler) => {
31+
test.onFinished ||= []
32+
test.onFinished.push(handler)
33+
})
34+
3035
function createTestHook<T>(name: string, handler: (test: TaskPopulated, handler: T) => void) {
3136
return (fn: T) => {
3237
const current = getCurrentTest()
3338

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

37-
handler(current, fn)
42+
return handler(current, fn)
3843
}
3944
}

‎packages/runner/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export { startTests, updateTask } from './run'
22
export { test, it, describe, suite, getCurrentSuite, createTaskCollector } from './suite'
3-
export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed } from './hooks'
3+
export { beforeAll, beforeEach, afterAll, afterEach, onTestFailed, onTestFinished } from './hooks'
44
export { setFn, getFn, getHooks, setHooks } from './map'
55
export { getCurrentTest } from './test-state'
66
export { processError } from '@vitest/utils/error'

‎packages/runner/src/run.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,21 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) {
210210
}
211211
}
212212

213-
if (test.result.state === 'fail')
214-
await Promise.all(test.onFailed?.map(fn => fn(test.result!)) || [])
213+
try {
214+
await Promise.all(test.onFinished?.map(fn => fn(test.result!)) || [])
215+
}
216+
catch (e) {
217+
failTask(test.result, e, runner.config.diffOptions)
218+
}
219+
220+
if (test.result.state === 'fail') {
221+
try {
222+
await Promise.all(test.onFailed?.map(fn => fn(test.result!)) || [])
223+
}
224+
catch (e) {
225+
failTask(test.result, e, runner.config.diffOptions)
226+
}
227+
}
215228

216229
// if test is marked to be failed, flip the result
217230
if (test.fails) {

‎packages/runner/src/types/tasks.ts

+7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface TaskPopulated extends TaskBase {
2626
result?: TaskResult
2727
fails?: boolean
2828
onFailed?: OnTestFailedHandler[]
29+
onFinished?: OnTestFinishedHandler[]
2930
/**
3031
* Store promises (from async expects) to wait for them before finishing the test
3132
*/
@@ -296,6 +297,11 @@ export interface TaskContext<Task extends Custom | Test = Custom | Test> {
296297
*/
297298
onTestFailed: (fn: OnTestFailedHandler) => void
298299

300+
/**
301+
* Extract hooks on test failed
302+
*/
303+
onTestFinished: (fn: OnTestFinishedHandler) => void
304+
299305
/**
300306
* Mark tests as skipped. All execution after this call will be skipped.
301307
*/
@@ -305,6 +311,7 @@ export interface TaskContext<Task extends Custom | Test = Custom | Test> {
305311
export type ExtendedContext<T extends Custom | Test> = TaskContext<T> & TestContext
306312

307313
export type OnTestFailedHandler = (result: TaskResult) => Awaitable<void>
314+
export type OnTestFinishedHandler = (result: TaskResult) => Awaitable<void>
308315

309316
export type SequenceHooks = 'stack' | 'list' | 'parallel'
310317
export type SequenceSetupFiles = 'list' | 'parallel'

‎packages/vitest/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export {
88
afterAll,
99
afterEach,
1010
onTestFailed,
11+
onTestFinished,
1112
} from '@vitest/runner'
1213
export { bench } from './runtime/benchmark'
1314

‎test/core/test/on-finished.test.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { expect, it, onTestFinished } from 'vitest'
2+
3+
const collected: any[] = []
4+
5+
it('on-finished regular', () => {
6+
collected.push(1)
7+
onTestFinished(() => {
8+
collected.push(3)
9+
})
10+
collected.push(2)
11+
})
12+
13+
it('on-finished context', (t) => {
14+
collected.push(4)
15+
t.onTestFinished(() => {
16+
collected.push(6)
17+
})
18+
collected.push(5)
19+
})
20+
21+
it.fails('failed finish', () => {
22+
collected.push(7)
23+
onTestFinished(() => {
24+
collected.push(9)
25+
})
26+
collected.push(8)
27+
expect.fail('failed')
28+
collected.push(null)
29+
})
30+
31+
it.fails('failed finish context', (t) => {
32+
collected.push(10)
33+
t.onTestFinished(() => {
34+
collected.push(12)
35+
})
36+
collected.push(11)
37+
expect.fail('failed')
38+
collected.push(null)
39+
})
40+
41+
it('after', () => {
42+
expect(collected).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
43+
})

0 commit comments

Comments
 (0)
Please sign in to comment.