Jonathan Garbee

An engineer, for the web platform

Stubbing Global Methods in JavaScript

I have recently dug into building some data generators. Building out what is a personal version of faker that is catered to my own needs. The first thing to generate was random numeric strings. These are strings that only contain numbers, but could also support leading zeros.

Upon trying to test the implementation, one core problem came to light. Needing to have reproducible random numbers for specific tests. Previously when this issue has come about, I’ve implemented an injector to give a function to make the number. That way in the main code the function is Math.random. While from a test suite, it can be an anonymous function that gives a static value.

This time I wanted to use some of my recent experience with Sinon to try and do it properly for a web application. Rather than lean on Dependency Injection as my back-end experience has prepared me for; I wanted to continue to write the main code as cleanly as I could.

The solution was to stub Math.random directly. Then give it an order of calls with static responses. In the case of needing to test leading zeros, force the first number to be a zero and for the rest continue calling the original function.

Sample code for stubbing Math's random function.
import { describe, it, afterEach } from 'node:test';
import { strict as assert } from 'node:assert';
import { makeNumeric } from './numeric.js';
import sinon from 'sinon';

describe(makeNumeric.name, () => {
afterEach(() => {
// Follow normal practice and reset the sandbox
// after each run. That way tests don't affect
// one another.
sinon.restore();
});

it('Generates a numeric string with leading zeros', () => {
// Create the stub before calling the code in question
const stub = sinon.stub(Math, 'random');
// The first time the stub is called, force it to
// return the value `0`. This will hit the leading
// zero check in the function.
stub.onFirstCall().returns(0);
// Once we've gotten 0 out first, nothing else matters.
// Subsequent calls can go back to the source function
// that has been stubbed.
stub.callThrough();

const generatedNumber = makeNumeric(5, {
allowLeadingZeros: true,
});

assert.equal(generatedNumber.startsWith('0'), true);
assert.equal(generatedNumber.length, 5);
});
});