Use Jest test.each to avoid duplicating test blocks
Run one assertion against many inputs with Jest's test.each, keeping failures isolated and test files maintainable.
31 Dec 2025, 13:37 UTC

The maintenance tax of copy-pasted tests
Imagine you need to verify a discount function across eight order totals. The quick approach is to write eight test() blocks, each calling the same function with different arguments and expectations. A month later, when the discount rules change, you must edit every block. It is easy to miss one, and a single failure does not always make it obvious which input caused it.
Jest's test.each lets you define the inputs and expected outputs in a table. Jest generates a separate test for every row, so each case runs independently and reports its own result.
How the table becomes tests
test.each accepts a template literal where the first row names the columns. Each following row becomes one test, and Jest passes the row values into your callback as an object. You can also pass an array of arrays or an array of objects, but the template literal is usually the easiest to read.
The feature works with synchronous and asynchronous callbacks. For async tests, you must return the promise or use await; otherwise Jest can finish the test before the assertion runs and report a false pass.
Worked example: discount tiers
Suppose this function lives in discount.ts:
export function discountRate(orderTotal: number): number {
if (orderTotal < 50) return 0;
if (orderTotal < 100) return 5;
if (orderTotal < 200) return 10;
return 15;
}
Instead of eight test() blocks, write one parameterized test in discount.test.ts:
import { discountRate } from './discount';
test.each`
orderTotal | expectedRate
${0} | ${0}
${49} | ${0}
${50} | ${5}
${99} | ${5}
${100} | ${10}
${199} | ${10}
${200} | ${15}
${500} | ${15}
`('applies $expectedRate% discount for order total $orderTotal', ({ orderTotal, expectedRate }) => {
expect(discountRate(orderTotal)).toBe(expectedRate);
});
Run the file from the project root:
$ npx jest discount.test.ts
You should see one passing test line per row, each with a distinct name. If you change one expected rate to a wrong value and rerun, only that row's test should fail while the others pass. The total number of tests reported by Jest should equal the row count.
Async variant and the silent-pass trap
If the function under test returns a promise, return it from the callback:
test.each`
userId | expectedRole
${1} | ${'admin'}
${2} | ${'viewer'}
`('resolves role for user $userId', async ({ userId, expectedRole }) => {
const role = await fetchUserRole(userId);
expect(role).toBe(expectedRole);
});
Omitting await or the return statement can make Jest mark the test as passed before the promise settles, hiding real failures.
Trade-offs and limits
- Large tables slow startup and clutter CI logs. Each row is a separate test, so a very large table increases output volume and can make it harder to scan failures.
- Column mismatches throw at the
test.eachline. If the table header and callback arguments do not align, Jest reports a runtime error pointing to the table definition, not the failing row. - Debugging a single row can be noisy. Although failures are isolated, a huge table still produces many test names in the summary.
Verify the result
- Run
npx jest discount.test.tsand confirm the output lists one test per table row. - Change one expected value to an incorrect number, rerun, and confirm only that specific case fails.
- For async cases, remove
awaittemporarily, introduce a failing assertion, and confirm Jest still catches it; then restoreawait.
If you need to undo the change, restore the previous test file with git checkout -- discount.test.ts. This only reverts test code; it does not affect application behavior.
Data-driven tests are most valuable when the logic stays the same and only the inputs change. If each case requires a completely different setup, separate test() blocks are clearer.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.