How to test a function that output is random using Jest?

I've taken Stuart Watt's solution and ran with it (and got a bit carried away). Stuart's solution is good but I was underwhelmed with the idea of having a random number generator always spit out 0.5 - seems like there would be situations where you're counting on some variance. I also wanted to mock crypto.randomBytes for my password salts (using Jest server-side). I spent a bit of time on this so I figured I'd share the knowledge.

One of the things I noticed is that even if you have a repeatable stream of numbers, introducing a new call to Math.random() could screw up all subsequent calls. I found a way around this problem. This approach should be applicable to pretty much any random thing you need to mock.

(side note: if you want to steal this, you'll need to install Chance.js - yarn/npm add/install chance)

To mock Math.random, put this in one of the files pointed at by your package.json's {"jest":{"setupFiles"} array:

const Chance = require('chance')

const chances = {}

const mockMath = Object.create(Math)
mockMath.random = (seed = 42) => {
  chances[seed] = chances[seed] || new Chance(seed)
  const chance = chances[seed]
  return chance.random()
}

global.Math = mockMath

You'll notice that Math.random() now has a parameter - a seed. This seed can be a string. What this means is that, while you're writing your code, you can call for the random number generator you want by name. When I added a test to code to check if this worked, I didn't put a seed it. It screwed up my previously mocked Math.random() snapshots. But then when I changed it to Math.random('mathTest'), it created a new generator called "mathTest" and stopped intercepting the sequence from the default one.

I also mocked crypto.randomBytes for my password salts. So when I write the code to generate my salts, I might write crypto.randomBytes(32, 'user sign up salt').toString('base64'). That way I can be pretty sure that no subsequent call to crypto.randomBytes is going to mess with my sequence.

If anyone else is interested in mocking crypto in this way, here's how. Put this code inside <rootDir>/__mocks__/crypto.js:

const crypto = require.requireActual('crypto')
const Chance = require('chance')

const chances = {}

const mockCrypto = Object.create(crypto)
mockCrypto.randomBytes = (size, seed = 42, callback) => {
  if (typeof seed === 'function') {
    callback = seed
    seed = 42
  }

  chances[seed] = chances[seed] || new Chance(seed)
  const chance = chances[seed]

  const randomByteArray = chance.n(chance.natural, size, { max: 255 })
  const buffer = Buffer.from(randomByteArray)

  if (typeof callback === 'function') {
    callback(null, buffer)
  }
  return buffer
}

module.exports = mockCrypto

And then just call jest.mock('crypto') (again, I have it in one of my "setupFiles"). Since I'm releasing it, I went ahead and made it compatible with the callback method (though I have no intention of using it that way).

These two pieces of code pass all 17 of these tests (I created __clearChances__ functions for the beforeEach()s - it just deletes all the keys from the chances hash)

Update: Been using this for a few days now and I think it works pretty well. The only thing is I think that perhaps a better strategy would be creating a Math.useSeed function that's run at the top of tests that require Math.random


I used:

beforeEach(() => {
    jest.spyOn(global.Math, 'random').mockReturnValue(0.123456789);
});

afterEach(() => {
    jest.spyOn(global.Math, 'random').mockRestore();
})

It is easy to add and restores the functionality outside the tests.


Here's what I put at the top of my test file:

const mockMath = Object.create(global.Math);
mockMath.random = () => 0.5;
global.Math = mockMath;

In tests run from that file, Math.random always returns 0.5.

Full credit should go to this for the idea: https://stackoverflow.com/a/40460395/2140998, which clarifies that this overwrite is test-specific. My Object.create is just my additional extra little bit of caution avoiding tampering with the internals of Math itself.