2.3 Example: Mocking the global Date object
There are situations where new Date()
or Date.now
is used in application code. That code needs to be tested, and it’s always a struggle to remember how to mock/stub or spy on Date.now
/new Date
with Jest.
This section goes through 5 approaches to mocking, stubbing and spying on the date constructor using Jest. This is a great example of how one might stub out a global object, constructor or method.
Date.now vs new Date(): the importance of types and function signatures
Date.now()
returns the unix time, ie. “the number of milliseconds elapsed since January 1, 1970 00:00:00 UTC.” (see Date.now on MDN).
new Date()
returns a new Date object, and behaves differently based on the input passed to it. If called with nothing, it returns the current Date.
The following are equivalent, although the values stored in the two variables will be strictly different.
const now = new Date();
const explicitNow = new Date(Date.now());
The values are strictly different because the “now” is calculated at different times, but since the Date constructor (new Date()
) supports passing a unix time to it, the two are equivalent.
Using new Date(Date.now())
makes for code that is a lot easier to test. Mocking a function that returns a number (like Date.now) is a lot easier than mocking a constructor.
Using jest fake timers and jest.setSystemTime
This functionality is only available when using Jest with “modern” timers (ie. not legacy). “modern” timers are opt-in from Jest 26 and the default from Jest 27.
test('It should create correct now Date', () => {
const date = new Date('2023-05-14');
jest.useFakeTimers().setSystemTime(date);
expect(new Date()).toEqual(date);
});
The test output is as follows, showing that we successfully mocked Date.now
:
npx jest src/02.03-date-set-system-time.test.js
PASS src/02.03-date-set-system-time.test.js
✓ It should create correct now Date (2 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
This is quite specific to Jest or other environments where we’re provided hooks to mock the date (eg. Node.js now has this functionality built into the node:test
package), we’ll now look at some other generalised approaches to mocking and stubbing globals.
Replacing Date.now with a stub
This isn’t really a Jest-specific trick. We’ll access the Node.js global
object and replace Date.now
with a stub.
To be good unit-testing citizens, we’ll put the original global.Date.now
implementation back after the test has run.
const literallyJustDateNow = () => Date.now();
test('It should call and return Date.now()', () => {
const realDateNow = Date.now.bind(global.Date);
const dateNowStub = jest.fn(() => 1530518207007);
global.Date.now = dateNowStub;
expect(literallyJustDateNow()).toBe(1530518207007);
expect(dateNowStub).toHaveBeenCalled();
global.Date.now = realDateNow;
});
The test output is as follows, showing that we successfully mocked Date.now
:
npx jest src/02.03-date-now-stub.test.js
PASS src/02.03-date-now-stub.test.js
✓ It should call and return Date.now() (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Spy on Date.now and add a mock implementation
A terser implementation of replacing Date.now
with a stub would be to use jest.spyOn(global.Date, 'now').mockImplementation()
.
Our mockImplementation will use a hard-coded date initialised using new Date('valid-date-string')
and return valueOf()
, which corresponds to the unix time of that date.
This has the advantage of not having to deal with replacing the real date or putting it back since we use mockImplementationOnce
.
const getNow = () => new Date(Date.now());
test('It should create correct now Date', () => {
jest
.spyOn(global.Date, 'now')
.mockImplementationOnce(() =>
new Date('2019-05-14T11:01:58.135Z').valueOf()
);
expect(getNow()).toEqual(new Date('2019-05-14T11:01:58.135Z'));
});
The test output is as follows, the test still passes.
npx jest src/02.03-date-now-spy.test.js
PASS src/02.03-date-now-spy.test.js
✓ It should create correct now Date (5ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Mock the whole Date class with a fixed date instance
So far we’ve just replaced the .now
function and changed its output.
An alternative approach is to override the global.Date
. Instead of global.Date
pointing to the actual Date, we replace it with our own mock class.
Mocking the whole class is a bit heavy-handed and might create tests that just test nothing.
For example for each Date
method we are using in the code, we’ll be tempted to override it with a stub instead of letting it fall through to the extended Date
class.
The advantage however is that for this example test, we are being very precise and only overriding behaviour of Date when it’s being initialised without any value being passed to it.
const getCurrentDate = () => new Date();
let realDate;
test('It should create new date', () => {
// Setup
const currentDate = new Date('2019-05-14T11:01:58.135Z');
realDate = Date;
global.Date = class extends Date {
constructor(date) {
if (date) {
return super(date);
}
return currentDate;
}
};
expect(getCurrentDate()).toEqual(new Date('2019-05-14T11:01:58.135Z'));
// Cleanup
global.Date = realDate;
});
This test also passes as per the following test output.
npx jest src/02.03-mock-date-class.test.js
PASS src/02.03-mock-date-class.test.js
✓ It should create new date (4ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Spy on new Date() constructor and add a mock implementation
One fun aspect of constructors in JavaScript is that they’re really “just functions”. Which means we can spy them using jest.spyOn
.
That means that we can spy on global.Date
and set a mockImplementation, which can be a hardcoded new Date()
.
This is nice and terse, the code relies on JavaScript hoisting. The hardcoded new Date()
is a call to the function that is being spied on, so it seems a bit counterintuitive that this works.
const getCurrentDate = () => new Date();
test('It should create new date', () => {
jest
.spyOn(global, 'Date')
.mockImplementationOnce(() => new Date('2019-05-14T11:01:58.135Z'));
expect(getCurrentDate()).toEqual(new Date('2019-05-14T11:01:58.135Z'));
});
It works as expected, as per test run output.
npx jest src/02.03-spy-date-constructor.test.js
PASS src/02.03-spy-date-constructor.test.js
✓ It should create new date (5ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Aside: a look at the implementation of jest-mock-now
The full code of the jest-mock-now
package is the following (see the code at github.com/mattiaerre/jest-mock-now):
const { NOW } = require('./constants');
module.exports = date => {
const now = date ? date.getTime() : NOW;
Date.now = jest.spyOn(Date, 'now').mockImplementation(() => now);
return now;
};
This is quite neat but having an obvious mock being set up and torn down is probably quite useful.
The different permuation on mocking Date.now
vs the Date
constructor illustrate different techniques that can be applied to mocking global objects and functions in JavaScript with Jest.
The following section will look at how to stub out a parent class when application code inherits from it.