107

How do I loop through dynamic test cases in Jest?

I have test cases like the following how do I dynamically create jest test case using it/test methods.

Here is what I have tried , However it just passes without excuting the test cases in the loop.

    const mymodule = require('mymodule');

    const testCases = [
        {q: [2, 3],r: 5},
        {q: [1, 2],r: 3},
        {q: [7, 0],r: 7},
        {q: [4, 4],r: 8}
    ];

    describe("Test my Math module", () => {
        test("test add method", () => {
            for (let i = 0; i < testCases.length; i++) {
                const { q,r } = testCases[i];
                it(`should  add ${q[0]},${q[1]} to ${expected}`, () => {
                    const actual = mymodule.add(q[0] + q[1]);
                    expect(actual).toBe(expected);
                });
            }
        });
    
    });
mehari
  • 3,057
  • 2
  • 22
  • 34

7 Answers7

154

There's an in-built way to do this: test.each(table)(name, fn, timeout)

e.g.

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

where each inner array in the 2D array is passed as args to the test function.

James Skemp
  • 8,018
  • 9
  • 64
  • 107
justin
  • 3,357
  • 1
  • 23
  • 28
  • @nickang yes it can be useful. I have also found use of `expect(result).toMatchInlineSnapshot()` to be useful if the output under test can be rendered to string (this doesn't have to be JSX, you can e.g. render a JSON data structure and snapshot match against it). – justin Jul 31 '21 at 15:04
29

If one passes, they all will. So you only need one for a positive test. Ideally, you want 1 positive where the 2 numbers equal the sum, and a failure where you pass a string or something and throw an error.

TL/DR; You don't need 4 tests that all do the same thing. Also, you need to pull the looped test cases out of the parent test for the loop to work.

This will work just fine:

import jest from 'jest';
import { sumModule } from './';

const tests = [
  {x: 1, y: 2, r: 3}, 
  {x: 3, y: 4, r: 7}
];

describe('Adding method', () => { 
    for(let i = 0; i < tests.length; i++){
      it('should add its params', () => {
        const actual = sumModule(tests[i].x, tests[i].y);
        expect(actual).toBe(tests[i].r);
      });
    }
});
James Skemp
  • 8,018
  • 9
  • 64
  • 107
sesamechicken
  • 1,928
  • 14
  • 19
  • 5
    Unfortunately the same is not possible when looping through dynamic test cases generated from an async operation. :( https://github.com/facebook/jest/issues/1619#issuecomment-358576732 – Kevin Farrugia Aug 22 '18 at 07:21
  • Yes. Doesn't work when iterating through async config, irrelevant of the method. :( – Kevin Farrugia Aug 24 '18 at 09:31
  • 4
    I have a code block async in beforeAll, and I want to generate test cases based on the results of the code block in the beforeAll. It keep write `Your test suite must contain at least one test.` – M.Abulsoud Nov 04 '19 at 07:40
  • 1
    "You don't need 4 tests that all do the same thing." is useless. In this minimum working example, he's testing an "add" function for which your argument is atp, but in the "real world", a function working with one set of inputs does not guarantee that it will work for all inputs. "...pull the looped test cases out..." is an excellent answer. – lmat - Reinstate Monica Aug 31 '22 at 12:52
19

If anyone wondering how this can work for a single input function here is an example

const testData = [
      ['input1', 'output1'],
      ['input2', 'output2'],
      ['input3', 'output3'],
]

test.each(testData)('myFunc work correctly for %s',(input, output) =>{
        expect(yourFunc(input)).toBe(output)
})

https://jestjs.io/docs/en/api#testeachtablename-fn-timeout

mehari
  • 3,057
  • 2
  • 22
  • 34
16

You can use classic JS, it is more readable and with less code:

[
  { input: [2, 3], output: 5 },
  { input: [1, 2], output: 3 },
].forEach(({ input, output }) => {
  it(`works correctly for ${input}`, () => {
    // ...

    expect(...).toBe(output);
  });
})
Tonatio
  • 4,026
  • 35
  • 24
  • 5
    This should be the answer. It's easier to write, read and work with. Being able to user interpolation on the test name is a huge advantage as well and enough for me not to use test.each. – GhostBytes Nov 15 '21 at 15:32
  • 2
    @GhostBytes you can interpolate the test string in test.each() - it has a kind of mix of sprintf and $varname depending on whether you use arrays or nested object test cases. https://jestjs.io/docs/api#1-testeachtablename-fn-timeout – scipilot Nov 19 '22 at 03:21
  • I love it! Is there any downside to this compared to test.each? I find this answer more readable. – MartijnvdB Feb 07 '23 at 07:59
  • 1
    I've been doing it this way for years. The "inbuilt" way is horrible IMO, because you have to stick to a positional argument format to fulfill the format string whereas with your own loop, you can name the tests however you want. The jest DSL works based on scope (as does almost everything in JS/TS) so there is no disadvantage to calling it in a loop as long as all of the it/test calls are invoked synchronously. Nitpick: I'd do it like @Gass does below so that you don't have to worry about ASI, but preceeding the bracket with a `;` would solve that as well. – Eric Haynes Feb 26 '23 at 04:12
8

An alternative solution:

const cases = [
  { q: [2, 3], r: 3 },
  { q: [1, 2], r: 3 },
  { q: [7, 0], r: 7 },
  { q: [4, 5], r: 8 }
];

describe("Test cases for math module", () => {
  cases.forEach((el, i) => {
    let value1 = el.q[0]
    let value2 = el.q[1]
    let value3 = el.r

    test(`the sum of ${value1} and ${value2} equals ${value3}`, () => {
      expect(value1 + value2).toBe(value3)
    })
  })
})

Test results

When all tests pass:

enter image description here

In case one or more tests fail, it will show like so:

enter image description here

I did all the tests in CodeSandbox, if you want to play with the code click here

Gass
  • 7,536
  • 3
  • 37
  • 41
  • Exactly how I've done it for years. Just because there's an "inbuilt" solution doesn't mean it's better. The `test.each` syntax is largely unreadable due to the format string restrictions. With this, you can make the data structure for `cases` in whatever format is most clear, and the whole thing is far easier to understand as a result. – Eric Haynes Feb 26 '23 at 04:17
  • Very useful answer ! I have a situation where I need to retrieve values from async factories and then use these values in several test cases. In your answer you are using static values and iterating them with .forEach(). There is a way to first retrieve values asynchronously and then use them like you did ? – Wuagliono May 12 '23 at 13:49
1

Instead of going with native JavaScript forEach way, It's better to use test.each() provided by Jest framework.

Below are some basic examples

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

Please note that we can pass array of primitives, array of arrays or array of objects to test.each()

When we pass array of primitives like ['a', 'b', 'c'], it internally converts to [['a'], ['b'], ['c']]

Below is another example by using Template Literal

test.each`
  a    | b    | expected
  ${1} | ${1} | ${2}
  ${1} | ${2} | ${3}
  ${2} | ${1} | ${3}
`('returns $expected when $a is added to $b', ({a, b, expected}) => {
  expect(a + b).toBe(expected);
});
Mr.7
  • 2,292
  • 1
  • 20
  • 33
0

For loop works in jest, No need to use inbuilt function without any reason!

 test('adding positive numbers is not zero', () => {
  for (let a = 1; a < 10; a++) {
    for (let b = 1; b < 10; b++) {
      expect(a + b).not.toBe(0);
    }
  }
});
Kunal Ranjan
  • 192
  • 1
  • 2
  • 13