Misc scribbles

Jest parallelization, globals, mocks, and squawkless tests

2021-10-05

I found that there is a little bit of confusion and misunderstanding around how things like parallelization work in jest, which sometimes leads to additional hacking around problems that may not exist or speculating incorrectly about test failure. This is also of course a point of concern when you have code that for some reason or another uses global variables. Here are a short summary of things that may cause confusion.

#Tests in a single file are NOT run in parallel

Simple example, the global variable r is included in the test condition, but it is accurately run in all cases because the tests are not run in parallel.

let r = 0
 
function timeout(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}
 
describe('tests', () => {
  it('t1', async () => {
    await timeout(1000)
    expect(r).toBe(0)
    r++
  })
  it('t2', async () => {
    await timeout(1000)
    expect(r).toBe(1)
    r++
  })
  it('t3', async () => {
    await timeout(1000)
    expect(r).toBe(2)
    r++
  })
})

This test will take 3 seconds, and will accurately count the global variable. If it was in parallel, it may only take 1 second, and would inaccurately count the global variable due to race conditions

#Tests in different files ARE run in parallel

Let's take another example where we use a global variable, and then two different tests use the global variable.

file_using_some_globals.js

let myGlobal = 0
 
export function doStuff() {
  myGlobal++
  return myGlobal
}
 
export function resetMyGlobal() {
  myGlobal = 0
}
 
export function timeout(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

test_global_vars1.test.js

import { doStuff, timeout } from './dostuff'
test('file1', async () => {
  doStuff()
  await timeout(1000)
  expect(doStuff()).toEqual(2)
})

test_global_vars2.test.js

import { doStuff, timeout } from './dostuff'
 
test('file1', async () => {
  await timeout(1000)
  expect(doStuff()).toEqual(1)
})

This test completes in less than 2 seconds, and these tests are run in parallel. They use different instances of the global state, and therefore have no worries with colliding their state.

#Does a mock from one test affect another test?

While seeking the fabled "squawk-less" test, it is often useful to mock console so that tests that produce an expected error don't actually print an error message. However, if not done carefully, you will remove errors across tests

So, could a mock from one test affect another test? If it's in the same file, yes!

mock_console.test.js

test('test1', () => {
  console.error = jest.fn()
  console.error('wow')
  expect(console.error).toHaveBeenCalled()
})
 
test('test2', () => {
  // this console.error will not appear because test1 mocked away console.error
  // without restoring it
  console.error("Help I can't see!")
})

To properly mock these, you should restore the console mock at the end of your function

test('test1', () => {
  const orig = console.error
  console.error = jest.fn()
  console.error('I should not see this!')
  expect(console.error).toHaveBeenCalled()
  console.error = orig
})
 
test('test2', () => {
  const consoleMock = jest.spyOn(console, 'error').mockImplementation()
  console.error('I should not see this!')
  consoleMock.mockRestore()
})
 
test('test3', () => {
  console.error('I should see this error!')
})

#Add-on: Achieve squawkless tests!

Your test output should just be a big list of PASS statements, not interleaved with console.error outputs from when you are testing error conditions of your code

"Squawkless tests" is a term I made up, but it means that if you have code under test that prints some errors to the console, then mock the console.error function, as in the previous section. Don't stand for having a bunch of verbose errors in your CI logs! However, I also suggest only mocking out console.error for tests that are expected to have errors, lest you paper over unexpected errors.

Figure: a nice clean test suite without a bunch of crazy console.error outputs

#Conclusion

Getting better at testing requires exercise, and understanding the basics of your tools can help! Hopefully this helps you achieve a better understanding and write cleaner jest tests.