Run the same Vitest assertion over a table with test.each
Use test.each to expand a single Vitest assertion into multiple isolated test cases from a data table, with readable names and normal filtering behavior.
09 May 2026, 05:10 UTC

Problem: duplicated assertions for the same logic
\nWhen a function must be correct for many inputs, copying a test block for each case creates noise and drift. Parameterized testing runs the same assertions against a table of inputs and reports failures per row. In Vitest this is test.each.
\nThe useful takeaway is to keep one assertion and one data table, get isolated test cases with names derived from the data, and keep filtering, watch mode and reporters working as with normal tests.
\nDesired outcome
\nA single test.each call that expands into multiple Vitest test cases, each named from the data values, each running in isolation. Failures point to the offending row without manual duplication.
\nPrerequisites
\n- \n
- A Vitest project with a test file using the Vitest globals or an import from 'vitest'. \n
- Node permissions to run commands in the project root. No elevated privileges are required. \n
- Test data that is immutable for the duration of the run. Mutating the source array during iteration can change later cases. \n
Write a parameterized suite with test.each
\ntest.each accepts an array of data and a callback. Vitest creates a test case per element and interpolates values into the name template.
\n// sum.test.ts\nimport { describe, test, expect } from 'vitest'\n\nfunction add(a: number, b: number) { return a + b }\n\ndescribe('add', () => {\n test.each([\n [1, 2, 3],\n [2, 3, 5],\n [-1, 1, 0],\n ])('adds %i + %i = %i', (a, b, expected) => {\n expect(add(a, b)).toBe(expected)\n })\n})\n\nThe %i, %s and %p placeholders are replaced with the row values for the test name. Using an array of arrays keeps the mapping positional and explicit.
\nObject rows improve readability when inputs are complex.
\ntest.each([\n { a: 1, b: 2, expected: 3 },\n { a: 2, b: 3, expected: 5 },\n])('adds $a + $b = $expected', ({ a, b, expected }) => {\n expect(add(a, b)).toBe(expected)\n})\n\nAvoid referencing closure-captured variables that change between iterations. Each generated test should depend only on its parameters.
\nControl execution with modifiers
\nModifiers apply to the whole generated suite.
\ntest.skip.each([...])('name', ...)\ntest.only.each([...])('name', ...)\n\nUse skip to temporarily exclude a data-driven suite, and only to focus the run on that suite while developing. Filtering by name with -t still works because each case has its own name.
\nExpected checks
\nRun from the project root:
\nnpx vitest run sum.test.ts\n\nInspect the test report. Each data row should appear as a separate test case with the interpolated name. A failure should be reported against the specific row values, not the whole suite.
\nFor async code, return or await the promise inside the callback. Nested async functions that are not awaited can produce unhandled promise rejections.
\ntest.each(urls)('fetches %s', async (url) => {\n const res = await fetch(url)\n expect(res.ok).toBe(true)\n})\n\nLimitations and recovery
\nData mutation risk. If the test data array is mutated during iteration, subsequent cases may see changed values or the runner may behave unexpectedly. Keep the source data const and avoid in-place modifications inside the callback.
\nClosure capture risk. Variables defined outside test.each that are mutated between runs will be shared. Prefer passing all needed values via the data table.
\nDebugging a failing row. Temporarily narrow the table to the failing row or use test.only.each with a reduced data set to isolate behavior, then restore the full table.
\nVerification step. After changes, run the file again and confirm the number of generated cases matches the number of data rows and that names reflect the current values.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.