14

Is there a reason I would prefer one method over the other? Here are some examples:

describe('some tests', () => {
  [1, 2, 3].forEach(num => {
    test(`${num}: test`, () => {
      doSomeTestStuff(num)
    }) 
  })

  // vs.

  test.each([1, 2, 3])('%s: test', (num) => {
    doSomeTestStuff(num)
  })
})

It seems kind of difficult to read the test.each syntax, especially when you can just do native javascript iteration to achieve what seems to be the same effect. Teardown/setup still happens the same way (from what I can tell).

Mike
  • 585
  • 1
  • 10
  • 24

3 Answers3

4

I would prefer the vanilla JS forEach version, because it's just JavaScript. This means:

  • You don't need to be familiar with Jest specifically, you don't need to go looking in the docs for what exactly test.each does. The test.each`table` form particularly can behave in a way that's surprising, see e.g. Jest test.each with literal table: "Not enough arguments supplied for given headings".
  • Equally it's portable between test frameworks, you can use the same approach in Mocha, Tap, etc.
  • You get proper IDE support - you need to wait for runtime to find out whether '.add($a, $b)' actually matches the properties in the objects, whereas `.add(${a}, ${b})` can be checked and autocompleted statically (and manipulated however you like, as you can interpolate arbitrary expressions).

It's not true that there's any difference in the way the tests are executed or reported. The forEach loop runs at test discovery time, and all three tests are registered. Then they're all run, separately, at execution time. You'd only see problems with failures impacting other cases if you moved the loop inside a single test case, e.g.:

describe('some bad tests', () => {
  // this an example of how not to do it
  test('num tests', () => {
    [1, 2, 3].forEach(num => {
      doSomeTestStuff(num)
    }) 
  })
})

One other thing to note with the vanilla forEach approach is that it is common, as you've shown, to inline the arrays of test cases. If you're relying on ASI, as your examples suggest you are, this sometimes means you need to prefix the array with ;:

describe('some tests', () => {
  [1, 2, 3].forEach(num => {
    test(`${num}: test`, () => {
      doSomeTestStuff(num)
    }) 
  })

  ;[4, 5, 6].forEach(num => {
    test(`${num}: test`, () => {
      doSomeTestStuff(num)
    }) 
  })
})

Without this, you'd get TypeError: Cannot read property '6' of undefined.

This is maybe a vote in Jest's favour, but if you're not going to use semicolons you do need to know when they will and won't get inserted - I'd argue it's a vote in semicolons' favour.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
3

Great question!

On the surface it would seem like the two are basically equivalent, but there are a few reasons why you may want to use .each instead.

  • If an expectation in a forEach fails, it'll fail immediately and stop execution. This leaves you in the dark on whether the other tests in the forEach passed or failed. With .each it is basically writing a separate and distinct test for each item in your array.
  • If an expectation in a forEach fails, it's not always obvious which item in the array caused the failure. In a .each you automatically get context from the unique test name.
  • With .each you can use tagged template literals to handle multiple parameters easily, and as parameters grow the .each can be very expressive and much more readable than an array with arbitrary values.
  • Since, as I said before, .each creates a separate test for each item, the final report will make it seem like you wrote more tests than you actually have. Just a small thing that brings satisfaction when you look at the final output.
Scott Hampton
  • 394
  • 4
  • 6
  • "If an expectation in a forEach fails, it'll fail immediately and stop execution." That doesn't seem to be the case. https://codesandbox.io/s/jest-playground-forked-0v47q?file=/src/sum.test.js You may configure Jest to bail after x number of tests failed, but that's not the default behavior. The default behavior is described here: https://jestjs.io/docs/configuration#bail-number--boolean – Kim Skovhus Andersen Aug 26 '21 at 09:17
  • 1
    Sorry for the poor wording. I don't mean that the whole test file will stop execution, just that one test case so it won't complete the `forEach` loop. It will still continue onto the next test case. Hopefully that clears up the confusion! – Scott Hampton Nov 04 '21 at 18:01
  • I'm just wondering, why I see 1 test passed and 1 test failed in the codesandbox, both within the same forEach loop, when that's the case. – Kim Skovhus Andersen Jan 17 '22 at 14:42
  • 6
    The first and fourth bullets in this answer are inaccurate. The `forEach` completes, registering all three tests, _then_ they're executed. They're separate tests that will be executed and reported separately. A failure would only stop the rest of the cases if the loop was _inside_ the test case. For the second note the OP is setting unique names in the `forEach` too. – jonrsharpe May 04 '22 at 17:17
  • @jonrsharpe It seems like the second bullet isn't accurate either as OP's code also changes the test name as well. I am really curious what (if anything) `each` is doing that a `forEach` is not - the source code for it is much longer than a few lines! – Ben Jones Oct 11 '22 at 15:26
  • BTW @ScottHampton has done a great job explaining why .each is superior to a loop *inside* the test. It just doesn't address the two code snippets that OP posted. – Ben Jones Oct 11 '22 at 15:27
0

One advantage of test.each can be better tool support. I just noticed this with the VS Code plugin orta.vscode-jest.

Marco Eckstein
  • 4,448
  • 4
  • 37
  • 48