Reduce Test Duplication with Vitest’s test.each
Learn how Vitest’s test.each lets you drive many test cases from a single table, reducing copy‑pasted blocks while keeping clear titles and TypeScript‑inferred parameters.
04 Mar 2026, 02:05 UTC

The problem: repetitive test blocks
When a utility function must be checked against many inputs, writing a separate test call for each case leads to copy‑pasted blocks. Changing the assertion later requires editing every block, and the test file becomes hard to scan.
How Vitest’s test.each works
Vitest provides test.each(table)(titleTemplate, fn). The table is an array of rows; each row becomes one test. Vitest spreads array rows into the callback arguments and replaces %s placeholders in titleTemplate with the row values, producing a distinct title per test.
Worked example: slug normalizer
import { describe, test, expect } from 'vitest'
// Helper under test
function normalizeSlug(str) {
return str.trim().toLowerCase().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, '')
}
// Table of [input, expected] pairs
const cases = [
[' Foo Bar ', 'foo-bar'],
['Hello_World', 'hello-world'],
['Mixed CASE 123', 'mixed-case-123'],
[' leading/trailing ', 'leading-trailing'],
]
test.each(cases)('normalizeSlug(%s) → %s', (input, expected) => {
expect(normalizeSlug(input)).toBe(expected)
})
With the tuple shape [string, string] TypeScript infers input and expected without extra annotations. If the table is large, you can import it from a separate file:
import cases from './slug.cases.ts'
test.each(cases)(...)
Verification steps
- Run
npx vitest run(orvitest runif locally installed) and watch the reporter list one test per row, each title showing the interpolated values. - Alter one row so the expectation fails; the failure message should contain that row’s interpolated title, helping you locate the exact case.
- Execute
tsc --noEmiton the test file to confirm TypeScript accepts the inferred parameters.
Trade‑offs and limits
- All rows share the same module state; if a test mutates a mock or a variable, reset it in
beforeEachorafterEachto avoid cross‑row leakage. - Long objects or arrays in a row make the generated title noisy. Prefer a short scalar identifier (e.g., an ID) in the row and keep detailed assertions inside the test body.
- Externalizing the table to JSON or a TypeScript fixture keeps the test file readable but adds an import step and a separate file to maintain.
Actionable closing
Start with a small table, confirm the reporter shows one test per row and that TypeScript infers the parameters, then grow the table or extract it to a fixture. Use describe.each when you need an entire suite parameterized, and fit.each/xit.each for temporary focus or skip.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.