Table-Driven Tests in Vitest with test.each: A Practical Guide
Use Vitest's test.each to turn repetitive input/output assertions into a typed data table, with interpolated test names that pinpoint exactly which case failed.
07 Oct 2025, 04:03 UTC

Why parameterized tests pay off
You have a pure function — a parser, a validator, a formatter — and you want to check it against a dozen input/output pairs. Writing twelve separate test() blocks duplicates the same assertion logic and makes the file painful to extend. Vitest's test.each lets you declare the cases as data and the assertion once, so adding a case is a one-line change and failures report the exact row that broke.
This guide assumes Vitest 0.30.0 or later, where test.each gained its fully typed generic implementation. Check your version with npx vitest --version before relying on the TypeScript inference shown below.
Prerequisites
- A project with Vitest installed (
npm i -D vitest) and atestscript such asvitest run. - TypeScript configured if you want compile-time checking of the case rows (optional but recommended).
- A pure function to test — side-effect-free functions are the best fit for table-driven tests.
The two forms of test.each
Vitest supports two syntaxes. The array form is the one to reach for in TypeScript codebases:
import { describe, expect, test } from 'vitest';
import { add } from './math';
describe('add', () => {
test.each([
{ a: 1, b: 2, expected: 3 },
{ a: -1, b: 1, expected: 0 },
{ a: 0.1, b: 0.2, expected: 0.3 },
])('add($a, $b) = $expected', ({ a, b, expected }) => {
expect(add(a, b)).toBeCloseTo(expected);
});
});Each object in the array becomes one test case. The $a, $b, $expected placeholders in the title string are interpolated from the row, so the reporter shows add(-1, 1) = 0 rather than a generic name. That interpolation is what makes CI logs readable when one row fails.
The tagged-template form is terser but loses type inference on rows:
test.each`
a | b | expected
${1} | ${2} | ${3}
${2} | ${3} | ${5}
`('returns $expected for $a + $b', ({ a, b, expected }) => {
expect(add(a, b)).toBe(expected);
});Prefer the array-of-objects form unless the table is very wide and the template layout genuinely reads better.
TypeScript inference
With the array form, Vitest infers the row shape from the array literal. Destructuring { a, b, expected } in the callback is fully typed — autocomplete works, and a typo like expectd fails compilation. If you want an explicit contract, annotate the array:
const cases: Array<{ a: number; b: number; expected: number }> = [
{ a: 1, b: 2, expected: 3 },
];
test.each(cases)('add($a, $b)', ({ a, b, expected }) => {
expect(add(a, b)).toBe(expected);
});Annotating the array rather than the callback keeps the title interpolation working and gives you a single place to evolve the row shape.
Verifying the behavior
Run the suite from the project root (no special permissions needed):
npx vitest run math.test.tsExpected check: the reporter lists one test per row, each with its interpolated title, and all pass. Then deliberately corrupt one row — change an expected value — and run again. The failure output should name the exact parameter set, e.g. add(0.1, 0.2) = 0.4, which is the whole point: you know which case broke without adding console logs. Restore the row afterwards.
In watch mode (npx vitest), editing a single row re-executes the affected file; Vitest's incremental reruns operate at file level, so keep table-driven tests for one function in one file to keep feedback fast.
Common pitfalls
- Duplicate generated names. If two rows interpolate to the same title (for example, two rows where
$aand$bmatch), Vitest may dedupe or overwrite tests. Include a distinguishing field in the title, such as anameordescriptionproperty per row. - Mixing with describe.each. Nesting
test.eachinsidedescribe.eachmultiplies cases quickly and makes name collisions more likely. If you need a matrix, generate the combined array explicitly with aflatMapso every row is visible in one place. - Async rows. The callback can be
asyncand each row is awaited independently — fine for parameterized API tests, but keep rows independent; never let one case mutate state another depends on. - Non-serializable values in titles. Objects interpolate as
[object Object]. Pass a string label field for the title instead of relying on object interpolation.
When not to use it
If each case needs different setup, different mocks, or different assertion shapes, a table hides more than it helps — write separate tests. test.each shines when the assertion is identical and only the data varies.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.