Using each() to loop over test data

Using each() to Loop Over Test Data

It is very common in tests to want to test the same function but with different arguments.

You could copy and paste your test() (or it()) tests, and make the slight changes to your input data.

But often it is easier to use an array of data as test data that you pass into the same test function.

Here is a very simple example, checking that a function handles both true and false correctly.

const greeting = val =>
  val ? 'Hi' : 'Bye';

test.each([true, false])(
  'when input is %s',
  initialValue => {
    expect(greeting(initialValue)).toBe(
      initialValue ? 'Hi' : 'Bye'
    );
  }
);

(The example is a bit too simple to really show its power.)

This .each() syntax works in both Jest and Vitest.

There is another syntax too with backticks (see further down in the lesson).

Why use each()?

  • Reduce test code repetition
  • Clearly define multiple test scenarios
  • Improve test coverage with minimal code

Each syntax

There are two ways to use each():

  • Inline array of test cases (like the example above)
  • Tagged template literal for more readable tests

Inline Array Example

  • If you pass a 1-dimensional array (like [true, false] or [1, 2, 3]), .each() calls your test with each value as its argument.
    • BTW: internally, the framework temporarily transforms it to [[1], [2], [3]] so your callback always receives an array of arguments.
  • If you pass a 2-dimensional array (like [[1, 2, 3], [4, 5, 9]]), each inner array is spread into the callback parameters (e.g. a=1, b=2, expected=3).

If that isn't clear, here is an example of how to make 3 test runs, with 3 arguments for each call.

(The first test run will call with a=1, b=2, expected=3)

test.each([
  [1, 2, 3],
  [4, 5, 9],
  [10, 20, 30],
])(
  'adds numbers %d and %d correctly',
  (a, b, expected) => {
    expect(a + b).toBe(expected);
  }
);

You can reference the values in the test titles so each run has a unique name in your output; this makes it easier to identify which case failed.

  • %d formats numbers
  • %s formats strings
  • %p pretty-prints objects or any value (helpful for debugging)

Here is another example of a 1D test:

test.each(['banana', 'apple'])(
  'adds fruit to the list',
  newFruit => {
    expect(
      ['grapes', newFruit].join()
    ).toBe(`grapes,${newFruit}`);
  }
);

It is often common to pass in objects in the array, like this

test.each([
  { a: 1, b: 2, expected: 3 },
  { a: -5, b: 5, expected: 0 },
  { a: 10, b: 15, expected: 25 },
])(
  'adds $a + $b to equal $expected',
  inputObject => {
    const { a, b, expected } =
      inputObject;
    expect(a + b).toBe(expected);
  }
);

Tagged Template Literal Example

The other syntax, which is often more useful with multiple arguments, is the tagged template literal form.

  • The first line contains your headers, separated by |. Use identifiers that are valid JS variable names (so you can destructure them), e.g. a | b | expected.
  • Every line under that uses ${'some-value'} to define the value
  • The test function receives an object, with the values for that test run
  • In your test title, you can reference them easily with $columnName as shown in the example below
test.each`
  a     | b     | expected
  ${1}  | ${2}  | ${3}
  ${4}  | ${5}  | ${9}
  ${10} | ${20} | ${30}
`(
  'adds $a + $b = $expected',
  ({ a, b, expected }) => {
    expect(a + b).toBe(expected);
  }
);

Lesson Task

The test is half written... it just needs some data passed into each(...).

You can pass an array of subarrays. If the first subarray was [100, 10, 90] then it would get called with price = 100, discountPercent = 10, expectedResult = 90

Pass a few example arrays like this.

Note: not shown in the test, but you could also pass an array of objects, like [{price: 100, discountPercent: 10, expectedResult}] and change your test function to accept a single argument.

Ready to try running tests for this lesson?