Jest Testing with test.each

5 min read

Often, we end up creating multiple unit tests for the same unit of code to make sure it behaves as expected with varied input. This is a good practice, but it can be a little tedious to create what is essentially the same test multiple times. We copy a test, paste it and update the variables that matter. Maybe we do that several times. Then, if we need to update our tests, we update each copy of the test. The good news is, starting with version 23 of Jest, there is built-in support for creating data-driven tests.

Initial Setup

We're going to be testing a utility function called isPalindrome that takes in a string and returns a boolean to indicate if the string is a palindrome or not. The function is as follows.

export const isPalindrome = (word) =>
  word.toLowerCase() === word.toLowerCase().split('').reverse().join('')

In our test file, let's start with a handful is tests that verify our behavior.

import { isPalindrome } from './index'

describe('isPalindrome', () => {
  test('isPalindrome("Racecar") - true', () => {
    const input = 'Racecar'
    const result = isPalindrome(input)
    expect(result).toBe(true)
  })

  test('isPalindrome("Typewriter") - false', () => {
    const input = 'Typewriter'
    const result = isPalindrome(input)
    expect(result).toBe(false)
  })

  test('isPalindrome("rotor") - true', () => {
    const input = 'rotor'
    const result = isPalindrome(input)
    expect(result).toBe(true)
  })
})

All of these tests exercise the behavior of our utility, and they all pass. Looking at these tests, we can see that they are all pretty much the same test with the exception of the input and the expected boolean. Let's see how we can streamline this with Jest's test.each functionality.

Testing with an Array and test.each

With test.each we will provide an array of arrays. Each inner array will represent values for each test case. In our case, we will have a string value as input, and a boolean to represent the expected result.

Here's what our call to each will look like with our array of test values

import { isPalindrome } from './index'

describe('isPalindrome - each', () => {
  test.each([
    ['Racecar', true],
    ['Typewriter', false],
    ['rotor', true],
  ]) // TODO: Implement test code
})

The result of test.each is a function that we'll define our test in. Just like it or test, it takes a string as the test description and a callback for the test implementation.

We'll add the function invocation and then talk about the dynamic pieces:

import { isPalindrome } from './index'

describe('isPalindrome - each', () => {
  test.each([
    ['Racecar', true],
    ['Typewriter', false],
    ['rotor', true],
  ])('isPalindrome("%s") - %s', (input, expected) => {
    const result = isPalindrome(input)
    expect(result).toBe(expected)
  })
})

Test Descriptions

Our test description will be a string that supports positional placeholders %s. With this description, 'isPalindrome("%s") - %s' and this array ['Racecar', true], our test description will be 'isPalindrome("Racecar") - true'. This allows us to give each test a specific description to track down issues when a case fails.

Test Parameters

Our array elements for each test case are passed into the test callback as arguments. When we use the same ['Racecar', true] array, (input, expected) => {...} will be called with "Racecar" as the input argument, and true as the expected argument.

From there, we just use those variables in our test callback to create the dynamic test.

This makes it easy to add additional test cases by just extending our nested array structure.

import { isPalindrome } from './index'

describe('isPalindrome - each', () => {
  test.each([
     ['Racecar', true],
     ['Typewriter', false],
-    ['rotor', true]
+    ['rotor', true],
+    ['kayak', true],
+    ['Step on no pets', true],
+    ['whatever', false],
  ])('isPalindrome("%s") - %s', (input, expected) => {
    const result = isPalindrome(input)
    expect(result).toBe(expected)
  })
})

Using test.each with Template Literals

In addition to the nested array syntax, test.each also allows us to define our inputs as a table, defined in a template literal. The same test from above can be written like this.

describe('isPalindrome - each template literal', () => {
  test.each`
    input                | expected
    ${'Racecar'}         | ${true}
    ${'Typewriter'}      | ${false}
    ${'rotor'}           | ${true}
    ${'kayak'}           | ${true}
    ${'Step on no pets'} | ${true}
    ${'whatever'}        | ${false}
  `('isPalindrome("$input") - $expected', ({ input, expected }) => {
    const result = isPalindrome(input)
    expect(result).toBe(expected)
  })
})

Here, we use test.each as a tagged template literal. The first row in our table defines the names for our input, and in each row, we define the values as placeholders with the ${ } syntax.

The placeholders in the test description now use the $fieldName syntax, and the arguments for the test callback are keys in a single object that is passed in, so we use destructuring here to use the same test body as we did in the nested array version.

You can see all of three versions of these tests running in this CodeSandbox and if video is your thing You can watch the video version of this lesson on egghead.io.

Conclusion

Not everything will fall into a data-driven approach, and we should always try to keeps tests small, focused, and readable. When you do run into a scenario where you need several instances of the same test with different values, test.each is a great approach!