Skip to content

Add typesafe hooks (beforeEach etc.) to TestAPI's #8617

@ysfaran

Description

@ysfaran

Clear and concise description of the problem

As a developer using Vitest I want to write setup/teardown logic that should just run for a single test file or describe block.
I would prefer to use beforeEach and afterEach hooks on top of fixtures, as they keep everything concise and readbale:

import { withDb as test } from "../fixtures

describe("My Integration Tests", () => {
  beforeEach(()=>{
     // ...
  })

  afterEach(()=>{
     // ...
  })
 
  test(...)
})

By default both hooks don't provide a type-safe context, so you can't just use fixtures in them like this:

beforeEach(({ db })=>{ // ERROR: db doesn't exist
   db.put(...)
   // ...
})

Although db actually exists in the test context, it doesn't exist for TypeScript.

Suggested solution

It would be great if TestAPI's would expose all hooks in a type-safe manner

import { withDb as test } from "../fixtures

test.beforeEach(({ db })=>{ // should be typesafe now
   // ...
})

I actually wrote a helper to make this work so the implementation is very straightforward, as it's only about the types:

export type Fixtures<T extends TestAPI<any>> = T extends TestAPI<infer U> ? U : never
export type TypeSafeHooks<T extends TestAPI<any>> = {
  beforeEach: typeof beforeEach<Fixtures<T>>
  afterEach: typeof afterEach<Fixtures<T>>
  beforeAll: typeof beforeAll
  afterAll: typeof afterAll
}

export const attachTypeSafeHooks = <T extends TestAPI<any>>(testApi: T): T & TypeSafeHooks<T> => {
  return Object.assign(testApi, {
    beforeEach: beforeEach<Fixtures<T>>,
    afterEach: afterEach<Fixtures<T>>,
    beforeAll: beforeAll, 
    afterAll: afterAll,
  })
}

Usage:

import { withDb  } from "../fixtures

const test = attachTypeSafeHooks(withDb)

test.beforeEach(({ db })=>{ // typesafe now
   // ...
})

It would be great if vitest would attach these hooks by default, so you can simply use test.beforeEach etc.

If you agree on the suggested solution, I'm glad to work on a PR.

Alternative

Alternative 1

The easiest way to make this work is by using global hooks and pass a generic:

import { withDb as test } from "../fixtures

type Fixtures<T extends TestAPI<any>> = T extends TestAPI<infer U> ? U : never
type WithDbFixtures = Fixtures<typeof withDb>

beforeEach<WithDbFixtures>(({ db })=>{ // works
   // ...
})

This is obviously not the biggest deal, but reduces the developer experience quite a bit for me. I want to make writing tests as pleasant as possible, as most of the folks don't like writing test anyway 😄

It's also prone to errors, if you pass the wrong generic type.

Alternative 2

The other alternative would be to add auto fixtures:

import { withDb as test } from "../fixtures

const test = withDb.extend({
  forEach: [
    async ({ db }, use) => {
      console.log("beforeEach")
      await use(undefined)
      console.log("afterEach")
    },
    { auto: true },
  ],
  forAll: [
    async ({ db }, use) => {
      console.log("beforeAll")
      await use(undefined)
      console.log("afterAll")
    },
    { auto: true, scope: "file" },
  ],
})

This is even worse in terms of developer experience, as it bloats up test files quite fast and make them less readable, especially if you have multiple different setup/teardown logic in one file.

Additional context

As seen in this example, playwright also supports this behaviour. They go even a step further, by making beforeAll and afterAll hooks generic and type-safe by only including worker fixtures as context.
I actually worked a lot with playwright and was missing the feature in vitest, hence I opened this feature request.

It's also a bit confusing that the official test context docs say, that beforeEach and afterEach hooks are deprecated and won't work with test.extend, although they actually do. Would be great if this could also be clarified.

Validations

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions