Search code examples
javascriptunit-testingjestjsmocking

JavaScript mocking with Jest - Mocking a dependent function


I have two modules written in JavaScript. In one module, I have a function that generates a random number, and in the other, a function that selects an element from an array based on this random number. An example is below:

randomNumber.js

export function generateRandomNumber() {
  return a random number
}

selectElement.js

import { generateRandomNumber } from './randomNumber.js'

export function selectRandomElement() {
  const myArray = ['a', 'b', 'c', 'd']
  const randomNumber = generateRandomNumber()

  return myArray[randomNumber]
}

The above is sudo-code. I've simplified the actual code.

I would like to use Jest to test the selectElement.js module. This module depends on the generateRandomNumber function in the other module. I want to mock that function so that every time I call selectRandomElement, I get the exact same result like so (again, sudo-code):

__tests__/selectElement.test.js

import { selectRandomElement } from '../selectElement.js'

const generateRandomNumber = () => 0  // use this as the mock

test('check we get the letter a', () => {
  expect(selectRandomElement()).toBe('a') //  returns 'a' (the first element from the array)
  expect(selectRandomElement()).toBe('a') //  returns 'a' (the first element from the array)
  expect(selectRandomElement()).toBe('a') //  returns 'a' (the first element from the array)
  expect(selectRandomElement()).toBe('a') //  returns 'a' (the first element from the array)
});

My intention is to produce consistent behavior in the tests. i.e. it always returns 'a' as when I call selectRandomElement in the tests I will always get the first element of the array as it's using the mocked function.

Is it possible to do this?

I would like to use ESM, and leave the structure of the modules as is. No refactoring. Only the test file can be changed.


Solution

  • I have work this out. My original mock code was a bit off. There are various things to change to make this work:

    1. jest.mock(... needs to be changed to jest.unstable_mockModule(.... Mock was not in my original code, but it is probably in your if you are having this issue. See below if not for what to do.
    2. You must use dynamic imports. ie const { generateRandomNumber } = await import('../randomNumber.js'). You must include await before import().
    3. The dynamic import must be below the mock deceleration.
    4. You must have __esModule: true defined in the mock.
    5. You must have "transform": {} in your jest config file. See list item 1 here for details. This needs to be changed if you are using Typescript. This is not specific for mocking. I add it for completeness.

    You do not need to change any of the source code (ie the non-test code) to accommodate the changes.

    Here is my test script above with all the updates needed.

    __tests__/selectElement.test.js

    import { selectRandomElement } from '../selectElement.js'
    
    jest.unstable_mockModule('../randomNumber.js', () => ({
      __esModule: true,  // Point 4 in the list above
      generateRandomNumber: jest.fn(() => 0)
    }))
    
    //  Point 3 in the list above. jest.unstable_mockModule is defined first.
    //  The dynamic import is defined second.
    const { generateRandomNumber } = await import('../randomNumber.js')  //  Point 2 in the list above
    
    test('check we get the letter a', () => {
      expect(selectRandomElement()).toBe('a')  //  returns 'a' (the first element from the array)
      expect(selectRandomElement()).toBe('a')  //  returns 'a' (the first element from the array)
      expect(selectRandomElement()).toBe('a')  //  returns 'a' (the first element from the array)
      expect(selectRandomElement()).toBe('a')  //  returns 'a' (the first element from the array)
    });
    

    This has taken me a long time to work out. It is a bit ridiculous that this is not better documented in the Jest docs. ESM has been around and integrated into Nodejs for a very long time now.