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

JavascriptJestjs

Javascript Problem Overview


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

import cuid from 'cuid';  
const functionToTest = (value) => ({
    [cuid()]: {
        a: Math.random(),
        b: new Date().toString(),
        c: value,
    }
});

So the output of functionToTest('Some predictable value') will be something like:

{
  'cixrchnp60000vhidc9qvd10p': {
    a: 0.08715126430943698,
    b: 'Tue Jan 10 2017 15:20:58 GMT+0200 (EET)',
    c: 'Some predictable value'
  },
}

Javascript Solutions


Solution 1 - Javascript

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.

Solution 2 - Javascript

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.

Solution 3 - Javascript

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

Solution 4 - Javascript

I'd ask myself the following questions:

  • Do I really need to test random output? If I have to, I'd most likely test ranges or make sure that I received a number in a valid format, not the value itself
  • Is it enough to test the value for c?

Sometimes there is a way to encapsulate the generation of the random value in a Mock and override the generation in your test to return only known values. This is a common practice in my code. https://stackoverflow.com/questions/28504545/how-to-mock-a-constructor-like-new-date#28507790 sounds like a similar approach in jestjs.

Solution 5 - Javascript

You could always use jest-mock-random

But it offers a little bit more functionality than mocking it as proposed in the first answer.

For example you could use before the testmockRandomWith(0.6); and your Math.random in test will always return this predictable value

Solution 6 - Javascript

Mocking literal random data isn't exactly the way to test. The question is, as it's stated, is a bit broad because "how to test a function that output is random" requires you to do statistical analysis of the output to ensure effective randomness - which has likely been done by the creators of your pseudo-random number generator.

Inferring instead "that output is random" means you want to ensure the function functions properly regardless of the random data then merely mocking the Math.random call to return numbers that meet your specific criteria (covering any variance) is enough. That function is a third-party boundary that, while needing testing, is not what's being tested based on my inference. Unless it is - in which case refer to the paragraph above.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBogdan SlovyaginView Question on Stackoverflow
Solution 1 - JavascriptRui FonsecaView Answer on Stackoverflow
Solution 2 - JavascriptStuart WattView Answer on Stackoverflow
Solution 3 - JavascriptTimView Answer on Stackoverflow
Solution 4 - JavascriptcringeView Answer on Stackoverflow
Solution 5 - JavascriptSirPeopleView Answer on Stackoverflow
Solution 6 - JavascriptRichard BarkerView Answer on Stackoverflow