Organize Test Cases using test.each in Jest

There is a better way to organize repetitive or similar test cases in Jest in a table to avoid writing out very similar tests where only specific input parameters change, and avoid duplicating code.

The simple math functions below are the functions to test in this example.

math-functions.js:

const isPerfectSquare = (x) => {
  return Number.isInteger(Math.sqrt(x))
}
 
const isProductPositive = (a, b) => {
  return a * b > 0
}
 
const realSqrt = (x) => {
  if (x < 0) throw new Error('Not defined for negatives')
  return Math.sqrt(x)
}
 
module.exports = { isPerfectSquare, isProductPositive, realSqrt }

For the first kind of test case layout,  we need to define a templated string in a format similar to the one below, listing the input and output for our test cases.

const { isPerfectSquare } = require('./math-functions.js')

const testCases = test.each`
 input     | expectedOutput
 ${'25'}   | ${true}
 ${'144'}  | ${true}
 ${'1024'} | ${true}
 ${'55'}   | ${false}
 ${'0'}    | ${true}
`

describe('perfect square tests', () => {
  testCases('it should return $expectedOutput when input is: $input',
    ({ input, expectedOutput }) => {
      expect(isPerfectSquare(input)).toBe(expectedOutput)
    })
})

Using test.each with Multiple Arguments per Test Case

The template below is used when we need multiple arguments passed into our function for each test case.

const { isProductPositive } = require('./math-functions.js')

const testCases = test.each`
 a       | b       | expectedOutput
 ${'5'}  | ${'20'} | ${true}
 ${'-1'} | ${'7'}  | ${false}
 ${'-4'} | ${'-4'} | ${true}
 `

describe('positive product tests', () => {
  testCases('it should return $expectedOutput for inputs: $a, $b',
    ({ a, b, expectedOutput }) => {
      expect(isProductPositive(a, b)).toBe(expectedOutput)
    })
})

Using a 2D Array of Inputs for Test Cases with test.each

We can use a two-dimensional array to define the test cases instead of the templated string if the cases are true/false or pass/fail; i.e. if the output should be the same for a varying set of inputs.

const { isProductPositive } = require('./math-functions.js')

const trueCases = [
  [5, 20],
  [-4, -4]
]

const falseCases = [
  [-1, 7]
]

describe('positive product tests: true cases', () => {
  test.each(trueCases)(
    'should return true for inputs: %s, %s',
    ( a, b ) => {
      expect(isProductPositive(a, b)).toBe(true)
    })
})

describe('positive product tests: false cases', () => {
  test.each(falseCases)(
    'should return false for inputs: %s, %s',
    ( a, b ) => {
      expect(isProductPositive(a, b)).toBe(false)
    })
})

Grouping Passing and Failing Test Cases

If we have a set of test cases which should pass and those which should throw an error, we can also use a nice clean layout for them.

const { realSqrt } = require('./math-functions.js')

const passingCases = [ 25, 3.141 ]
const errorCases = [ -1, -4.5 ]

describe('passing test cases', () => {
  test.each(passingCases)(
    'should return successfully for input: %s',
    (x) => {
      try {
        realSqrt(x)
      } catch (error) {
        fail('Should not reach here')
      }
    })
})

describe('expected error test cases', () => {
  test.each(errorCases)(
    'should throw error for input: %s',
    (x) => {
      try {
        realSqrt(x)
        fail('Error was not thrown')
      } catch(error) {
        expect(error.message).toBe('Not defined for negatives')
      }
    })
})

Note the fail() function to explicitly fail the test cases for undesired behaviour.