如何模拟像 new Date()这样的构造函数

我有一个方法,它依赖于 new Date来创建一个日期对象,然后对其进行操作。我正在测试操作是否按预期工作,因此我需要将返回的日期与预期日期进行比较。为了做到这一点,我需要确保 new Date在测试和正在测试的方法中返回相同的值。我该怎么做?

有没有一种方法可以实际模拟构造函数的返回值?

我可以创建一个模块,该模块可以与一个提供日期对象并且可以被模拟的函数一起使用。但在我的代码中,这似乎是一个不必要的抽象。

一个需要测试的示例函数。

module.exports = {
sameTimeTomorrow: function(){
var dt = new Date();
dt.setDate(dt + 1);
return dt;
}
};

如何模拟 new Date()的返回值?

122436 次浏览

Here's what I'm doing now and this is working and doesn't clutter my method's signature.

newDate.js

module.exports = function(){
return new Date();
};

someModule.js

var newDate = require('newDate.js');
module.exports = {
sameTimeTomorrow: function(){
var dt = newDate();
dt.setDate(dt.getDate() + 1);
return dt;
}
};

someModule-test.js

jest.dontMock('someModule.js');


describe('someModule', function(){


it('sameTimeTomorrow', function(){
var newDate = require('../../_app/util/newDate.js');
newDate.mockReturnValue(new Date(2015, 02, 13, 09, 15, 40, 123));


var someModule = require('someModule.js');


expect(someModule.sameTimeTomorrow().toString()).toBe(new Date(2015, 02, 14, 09, 15, 40, 123).toString());
});


});

You can use jasmine's spyOn (jest is built on jasmine) to mock Date's prototype for getDate as follows:

spyOn(Date.prototype, 'setDate').and.returnValue(DATE_TO_TEST_WITH);

SpyOn will also clean up after it's self and only lasts for the scope of the test.

You can replace the Date constructor with something that always returns a hardcoded date, and then put it back to normal when done.

var _Date = null;


function replaceDate() {
if (_Date) {
return
};


_Date = Date;


Object.getOwnPropertyNames(Date).forEach(function(name) {
_Date[name] = Date[name]
});


// set Date ctor to always return same date
Date = function() { return new _Date('2000-01-01T00:00:00.000Z') }


Object.getOwnPropertyNames(_Date).forEach(function(name) {
Date[name] = _Date[name]
});
}


function repairDate() {
if (_Date === null) {
return;
}


Date = _Date;
Object.getOwnPropertyNames(_Date).forEach(function(name) {
Date[name] = _Date[name]
});


_Date = null;
}


// test that two dates created at different times return the same timestamp
var t0 = new Date();


// create another one 100ms later
setTimeout(function() {
var t1 = new Date();


console.log(t0.getTime(), t1.getTime(), t0.getTime() === t1.getTime());


// put things back to normal when done
repairDate();
}, 100);

You can override Date constructor with an mocked function which returns your a constructed Date object with a date value you specified:

var yourModule = require('./yourModule')


test('Mock Date', () => {
const mockedDate = new Date(2017, 11, 10)
const originalDate = Date


global.Date = jest.fn(() => mockedDate)
global.Date.setDate = originalDate.setDate


expect(yourModule.sameTimeTomorrow().getDate()).toEqual(11)
})

You can test the example here: https://repl.it/@miluoshi5/jest-mock-date

If you have more than one Date (either in multiple tests or multiple times in one test) you might need to do the following:

const OriginalDate = Date;


it('should stub multiple date instances', () => {
jest.spyOn(global, 'Date');
const date1: any = new OriginalDate(2021, 1, 18);
(Date as any).mockImplementationOnce(mockDate(OriginalDate, date1));


const date2: any = new OriginalDate(2021, 1, 19);
(Date as any).mockImplementationOnce(mockDate(OriginalDate, date2));


const actualDate1 = new Date();
const actualDate2 = new Date();


expect(actualDate1).toBe(date1);
expect(actualDate2).toBe(date2);
});


function mockDate(OriginalDate: DateConstructor, date: any): any {
return (aDate: string) => {
if (aDate) {
return new OriginalDate(aDate);
}
return date;
};
}

Also see this answer


Original Answer:

I just wrote a jest test and was able to stub new Date() with global.Date = () => now

Update: this answer is the approach for jest < version 26 see this answer for recent jest versions.


You can mock a constructor like new Date() using jest.spyOn as below:

test('mocks a constructor like new Date()', () => {
console.log('Normal:   ', new Date().getTime())


const mockDate = new Date(1466424490000)
const spy = jest
.spyOn(global, 'Date')
.mockImplementation(() => mockDate)


console.log('Mocked:   ', new Date().getTime())
spy.mockRestore()


console.log('Restored: ', new Date().getTime())
})

And the output looks like:

Normal:    1566424897579
Mocked:    1466424490000
Restored:  1566424897608

See the reference project on GitHub.

Note: If you are using TypeScript and you would encounter a compilation error, Argument of type '() => Date' is not assignable to parameter of type '() => string'. Type 'Date' is not assignable to type 'string'. In this case, a workaround is to use the mockdate library, which can be used to change when "now" is. See this question for more details.

You can use date-faker to mock what new Date() or Date.now() returns.

import { dateFaker } from 'date-faker'; // var { dateFaker } = require('date-faker');


// will return tomorrow, shift by one unit
dateFaker.add(1, 'day');


// shift by several units
dateFaker.add({ year: 1, month: -2, day: 3 });


// set up specific date, accepts Date or time string
dateFaker.set('2019/01/24');


dateFaker.reset();

I'm using Typescript and the easiest implementaion I found was doing the following:

const spy = jest.spyOn(global, 'Date');  // spy on date
const date = spy.mock.instances[0];      // gets the date in string format

and then use new Date(date) for your tests

Although the other answers solve the problem, I find it more natural and generally applicable to mock only the Date's "parameterless constructor" behavior while keeping other features of Date intact. For instance, when ISO Date string is passed to the constructor, it is probably reasonable to expect that this specific date is returned as opposed to the mocked Date.

test('spies new Date(...params) constructor returning a mock when no args are passed but delegating to real constructor otherwise', () => {
const DateReal = global.Date;
const mockDate = new Date("2020-11-01T00:00:00.000Z");


const spy = jest
.spyOn(global, 'Date')
.mockImplementation((...args) => {
if (args.length) {
return new DateReal(...args);
}
return mockDate;
})
        

const dateNow = new Date();


//no parameter => mocked current Date returned
console.log(dateNow.toISOString()); //outputs: "2020-11-01T00:00:00.000Z"


//explicit parameters passed => delegated to the real constructor
console.log(new Date("2020-11-30").toISOString()); //outputs: "2020-11-30T00:00:00.000Z"
    

//(the mocked) current Date + 1 month => delegated to the real constructor
let dateOneMonthFromNow = new Date(dateNow);
dateOneMonthFromNow.setMonth(dateNow.getMonth() + 1);
console.log(dateOneMonthFromNow.toISOString()); //outputs: "2020-12-01T00:00:00.000Z"


spy.mockRestore();
});

Simply do this:

it('should mock Date and its methods', () => {
const mockDate = new Date('14 Oct 1995')
global.Date = jest.fn().mockImplementation(() => mockDate)
Date.prototype.setHours = jest.fn().mockImplementation((hours) => hours)
Date.prototype.getHours = jest.fn().mockReturnValue(1)
}

it's working for me

In my case I had to mock the whole Date and 'now' function before test:

const mockedData = new Date('2020-11-26T00:00:00.000Z');

jest.spyOn(global, 'Date').mockImplementation(() => mockedData);

Date.now = () => 1606348800;

describe('test', () => {...})

Since jest 26, you can use the 'modern' fakeTimers implementation (see article here) wich supports the method jest.setSystemTime.

beforeAll(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date(2020, 3, 1));
});


afterAll(() => {
jest.useRealTimers();
});

Note that 'modern' will be the default implementation from jest version 27.

See documentation for setSystemTime here.

Another way for older jest versions might be to create a Date subclass.

I only needed to mock the current date for my use case. The code under test uses new Date() and a lot of the real Date methods.

class MockDate extends Date {
constructor(arg) {
// use arg normally if provided,
// otherwise default to mock date: 2022-06-16T01:02:03.004
super(arg || 1655341323004);
}
}


global.Date = MockDate;

For those who simply need to have a static date when calling new Date(), it's possible to fake the system time with jest:

/**
* Fakes system time with the given timestamp.
* Don't forget to call `jest.useRealTimers()` after the test.
*/
export function setSystemTime(timestamp: number): void {
jest.useFakeTimers()
jest.setSystemTime(timestamp)
}

In beforeAll:

now = Date.now()
setSystemTime(now)

In afterAll:

jest.useRealTimers()