Tom MacWright

2025@macwright.com

Vitest with async fixtures and it.for/it.each

Pretty often in Val Town’s vitest test suite we have some scenario where we want to set up some data in the database and then use the test.each method to test different operations on the database. We finally did the work of figuring out how to represent this nicely today and came up with something like this:

import { describe, it as _it, expect } from "vitest";

describe("tests", () => {
  const it = _it.extend<{
    time: number;
  }>({
    // biome-ignore lint/correctness/noEmptyPattern: vitest is strict about this param
    time: async ({}, use) => {
      use(await Promise.resolve(Date.now()));
    },
  });

  c.for([[1, 2]])("should work", ([a, b], { time }) => {
    expect(a).toEqual(1);
    expect(b).toEqual(2);
    expect(time).toBeTypeOf("number");
  });
});

This required a few different bits of understanding:

  • it.for is a better it.each. We were using it.each, but it doesn't support text context.
  • Test context is a pretty decent way of storing fixtures. Before we were using beforeEach and beforeAll and assigning to variables that we'd refer to in test cases. it.extend is a bit smoother than that.
  • Still, manually repeating the types of the fixtures in it.extend is an annoyance, probably cause by the fact that you call use(value) instead of return value to assign fixtures. There's probably a good reason why they did that, but nevertheless I'd prefer to have these fixture values inferred.
  • Vitest is pretty wild in that it does static analysis checks, which is why I had to disable a biome rule here for that empty object pattern: vitest insists in destructuring, and biome does not like empty object destructuring.