test.each is not a magic bullet for writing tests, it’s a way to reduce boilerplate when you have many similar test cases that only differ by input and expected output.

Let’s see it in action. Imagine you need to test a simple sum function:

// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;

Without test.each, you’d write something like this:

// sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

test('adds -1 + 1 to equal 0', () => {
  expect(sum(-1, 1)).toBe(0);
});

test('adds 0 + 0 to equal 0', () => {
  expect(sum(0, 0)).toBe(0);
});

test('adds 100 + 200 to equal 300', () => {
  expect(sum(100, 200)).toBe(300);
});

This works, but it’s repetitive. If your sum function had more cases, this file would grow rapidly.

test.each lets you define your test cases in a table-like structure. The first argument to test.each is either a template literal string or an array, defining the parameters for your test. The second argument is the test function itself.

Here’s the same sum test suite using test.each:

// sum.test.js
const sum = require('./sum');

describe('sum function tests', () => {
  // Using a template literal for parameters
  test.each`
    a    | b    | expected
    ${1} | ${2} | ${3}
    ${-1} | ${1} | ${0}
    ${0} | ${0} | ${0}
    ${100} | ${200} | ${300}
  `('adds $a + $b to equal $expected', (a, b, expected) => {
    expect(sum(a, b)).toBe(expected);
  });

  // Using an array of arrays for parameters
  test.each([
    [1, 2, 3],
    [-1, 1, 0],
    [0, 0, 0],
    [100, 200, 300],
  ])('adds %p + %p to equal %p', (a, b, expected) => {
    expect(sum(a, b)).toBe(expected);
  });
});

When you run this with Jest, it will generate individual tests for each row in your table. The output will look something like this:

  sum function tests
    ✓ adds 1 + 2 to equal 3
    ✓ adds -1 + 1 to equal 0
    ✓ adds 0 + 0 to equal 0
    ✓ adds 100 + 200 to equal 300
    ✓ adds 1 + 2 to equal 3
    ✓ adds -1 + 1 to equal 0
    ✓ adds 0 + 0 to equal 0
    ✓ adds 100 + 200 to equal 300

Notice how Jest automatically names the generated tests based on the template literal string or the %p placeholders in the array version.

The template literal syntax is generally more readable for defining parameters because you can name them directly (e.g., a, b, expected). The ${value} syntax interpolates the values into the string, and the actual test function receives these interpolated values as arguments in the order they appear.

The array syntax is more concise. Each inner array represents a single test case, and the values within that array are passed as arguments to the test function. You use formatters like %p (pretty print) in the test description string to display the arguments.

You can also use test.only.each or test.skip.each just like their test counterparts.

The primary benefit of test.each is DRY (Don’t Repeat Yourself). It consolidates repetitive test logic into a single, data-driven structure, making your test suite cleaner, easier to read, and simpler to maintain. When you need to add a new test case, you just add another row to your table. When you need to change the logic, you change it in one place.

You can also use describe.each to parameterize entire describe blocks. This is useful if you have a suite of tests that needs to be run against different configurations or environments.

Consider a scenario where you’re testing a function that handles different data types or edge cases, like string manipulation or numerical precision. test.each shines when you have a significant number of distinct inputs and expected outputs that share the same testing logic. For instance, testing a capitalize function:

// capitalize.js
function capitalize(str) {
  if (typeof str !== 'string' || str.length === 0) {
    return '';
  }
  return str.charAt(0).toUpperCase() + str.slice(1);
}
module.exports = capitalize;
// capitalize.test.js
const capitalize = require('./capitalize');

describe('capitalize function tests', () => {
  test.each([
    ['hello', 'Hello'],
    ['world', 'World'],
    ['jest', 'Jest'],
    ['', ''], // Edge case: empty string
    ['a', 'A'], // Edge case: single character
    ['HELLO', 'HELLO'], // Already capitalized
  ])('capitalizes "%s" to "%s"', (input, expected) => {
    expect(capitalize(input)).toBe(expected);
  });

  // Testing non-string inputs
  test.each([
    [null, ''],
    [undefined, ''],
    [123, ''],
  ])('handles non-string input "%s" by returning ""', (input, expected) => {
    expect(capitalize(input)).toBe(expected);
  });
});

This approach makes it incredibly easy to add more test cases for different scenarios, like international characters, numbers as strings, or specific punctuation, without cluttering your test file.

The real power comes when you combine test.each with describe.each. Imagine testing a function that behaves differently based on a configuration flag:

// multiply.js
function multiply(a, b, config = {}) {
  if (config.strict && (typeof a !== 'number' || typeof b !== 'number')) {
    throw new Error('Strict mode requires numbers');
  }
  return a * b;
}
module.exports = multiply;
// multiply.test.js
const multiply = require('./multiply');

const configs = [
  { name: 'default config', options: {} },
  { name: 'strict mode', options: { strict: true } },
];

describe.each(configs)('with %s', ({ name, options }) => {
  test.each([
    [2, 3, 6],
    [-2, 3, -6],
    [0, 5, 0],
  ])('multiplies %p by %p to equal %p', (a, b, expected) => {
    expect(multiply(a, b, options)).toBe(expected);
  });

  if (options.strict) {
    test('throws error for non-numeric input in strict mode', () => {
      expect(() => multiply('a', 5, options)).toThrow('Strict mode requires numbers');
    });
  }
});

This setup generates two describe blocks: one for the "default config" and one for "strict mode." Within each describe block, the test.each runs its defined test cases. This allows you to systematically test your code’s behavior across different operational contexts without duplicating the core test logic.

One subtle but important aspect of test.each is how it handles asynchronous tests. If your test function returns a Promise, Jest will correctly wait for it to resolve. For example, testing an async fetchData function:

// fetchData.js
async function fetchData(id) {
  // Simulate an API call
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, data: `Data for ${id}` });
    }, 100);
  });
}
module.exports = fetchData;
// fetchData.test.js
const fetchData = require('./fetchData');

describe('fetchData tests', () => {
  test.each([
    [1, 'Data for 1'],
    [2, 'Data for 2'],
  ])('fetches data for ID %p', async (id, expectedData) => {
    const result = await fetchData(id);
    expect(result.id).toBe(id);
    expect(result.data).toBe(expectedData);
  });
});

The async keyword on the test function and the await call ensure that Jest waits for the fetchData Promise to resolve before moving on to the next assertion or test case. Jest intelligently detects Promises returned by test functions when using test.each.

The primary benefit of test.each is DRY (Don’t Repeat Yourself). It consolidates repetitive test logic into a single, data-driven structure, making your test suite cleaner, easier to read, and simpler to maintain. When you need to add a new test case, you just add another row to your table. When you need to change the logic, you change it in one place.

When using the array-based syntax for test.each, the formatters like %p are crucial for providing informative test descriptions. If you omit them or use incorrect formatters, the generated test names can become unhelpful, making it harder to pinpoint failures.

The next concept you’ll likely explore is how to integrate test.each with more complex data structures, such as objects or custom matchers, to further enhance your parameterized testing capabilities.

Want structured learning?

Take the full Jest course →