Skip to content

Jest Parameterized Tests

When several test cases follow the same pattern with different inputs and expected outputs, repeating nearly identical test() blocks becomes tedious and hard to maintain. This lesson covers test.each() and describe.each() for data-driven testing.

test.each() with an Array of Cases

test.each(table)(name, fn) runs the same test function once per row in table, substituting each row's values into both the callback's parameters and the test name (using %s, %i, %p, and similar placeholders). This turns many near-duplicate tests into one compact, easy-to-extend definition.

The table can be an array of arrays (positional arguments) or an array of objects (named properties, referenced with $propertyName in the test name).

test.each([
  [1, 1, 2],
  [2, 3, 5],
  [-1, 1, 0],
])('add(%i, %i) returns %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});

This single call produces three separate, individually-named test results: "add(1, 1) returns 2", and so on.

test.each() with Named Object Rows

test.each([
  { input: 'HELLO', expected: 'hello' },
  { input: 'World', expected: 'world' },
])('lowercase($input) returns $expected', ({ input, expected }) => {
  expect(input.toLowerCase()).toBe(expected);
});
  • Object rows are destructured directly in the callback's parameter.
  • $propertyName placeholders in the test name are replaced with that row's value.
  • %s, %i, %d, %p, %j are printf-style placeholders for array-based rows.
  • describe.each() works identically but wraps a whole describe() block per row instead of a single test.

Parameterized Tests Cheat Sheet

Common test.each()/describe.each() patterns.

Pattern Example
Array rows, positional args test.each([[1,2,3]])('%i+%i=%i', (a,b,e) => {})
Object rows, named args test.each([{a:1,b:2}])('$a+$b', ({a,b}) => {})
Grouped describe per case describe.each([...])('case %s', (x) => { test(...) })
Placeholder for strings %s
Placeholder for JSON %j or %p

When to Reach for test.each()

Parameterized tests work best when the *shape* of the test is identical across cases and only the input/output values differ — validation rules, formatting functions, and edge-case boundary checks are classic candidates. If each case actually needs different setup or assertions, separate test() blocks are clearer.

  • Good fit: testing a validator against many valid/invalid input strings.
  • Good fit: boundary testing a function across a range of numeric inputs.
  • Poor fit: cases that need meaningfully different setup, mocks, or assertions each time.

Grouping Related Cases with describe.each()

describe.each() is useful when each row needs multiple related tests, not just one — for example, testing several properties of the same computed value for each input case.

describe.each([
  { role: 'admin', canDelete: true },
  { role: 'member', canDelete: false },
])('permissions for $role', ({ role, canDelete }) => {
  test('canDelete matches expectation', () => {
    expect(hasPermission(role, 'delete')).toBe(canDelete);
  });
});

Common Mistakes

  • Using test.each() for cases that actually need different setup or mocking per row.
  • Forgetting the placeholder order in the test name must match the row's argument order.
  • Making the test data table so large it becomes hard to scan and review meaningfully.
  • Not naming rows descriptively enough, producing confusing generated test names.

Key Takeaways

  • test.each() runs the same test logic once per row of a data table.
  • Rows can be arrays (positional) or objects (named, referenced with $property).
  • describe.each() groups multiple related tests per row instead of just one.
  • Reserve parameterization for cases where only input/output values differ, not setup logic.

Pro Tip

When a bug report mentions a specific edge case, add it as a new row to an existing test.each() table instead of writing a whole new test — it keeps related cases together and documents the bug fix directly in the test data.