如何在Jest中设置模拟约会?

我使用moment.js在我的React组件的帮助文件中做大部分的日期逻辑,但我还没有弄清楚如何在Jest a la sinon.useFakeTimers()中模拟日期。

Jest文档只谈到了计时器函数,如setTimeoutsetInterval等,但没有帮助设置日期,然后检查我的日期函数是否做了它们应该做的事情。

这是我的一些JS文件:

var moment = require('moment');


var DateHelper = {
  

DATE_FORMAT: 'MMMM D',
API_DATE_FORMAT: 'YYYY-MM-DD',
  

formatDate: function(date) {
return date.format(this.DATE_FORMAT);
},


isDateToday: function(date) {
return this.formatDate(date) === this.formatDate(moment());
}
};




module.exports = DateHelper;

这是我用Jest设置的:

jest.dontMock('../../../dashboard/calendar/date-helper')
.dontMock('moment');


describe('DateHelper', function() {
var DateHelper = require('../../../dashboard/calendar/date-helper'),
moment = require('moment'),
DATE_FORMAT = 'MMMM D';


describe('formatDate', function() {


it('should return the date formatted as DATE_FORMAT', function() {
var unformattedDate = moment('2014-05-12T00:00:00.000Z'),
formattedDate = DateHelper.formatDate(unformattedDate);


expect(formattedDate).toEqual('May 12');
});


});


describe('isDateToday', function() {


it('should return true if the passed in date is today', function() {
var today = moment();


expect(DateHelper.isDateToday(today)).toEqual(true);
});
    

});


});

现在这些测试通过了,因为我使用了moment,我的函数也使用了moment,但它似乎有点不稳定,我想将日期设置为测试的固定时间。

你知道该怎么做吗?

230688 次浏览

MockDate可以在玩笑测试中使用,以改变new Date()返回的结果:

var MockDate = require('mockdate');
// I use a timestamp to make sure the date stays fixed to the ms
MockDate.set(1434319925275);
// test code here
// reset to native Date()
MockDate.reset();

由于momentjs在内部使用了Date,你可以重写Date.now函数来总是返回相同的时刻。

Date.now = jest.fn(() => 1487076708000) //14.02.2017

Date.now = jest.fn(() => new Date(Date.UTC(2017, 1, 14)).valueOf())

对于快速和肮脏的解决方案,使用jest.spyOn锁定时间:

let dateNowSpy;


beforeAll(() => {
// Lock Time
dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => 1487076708000);
});


afterAll(() => {
// Unlock Time
dateNowSpy.mockRestore();
});

更新:

更健壮的解决方案请看计时员:

import timekeeper from 'timekeeper';


beforeAll(() => {
// Lock Time
timekeeper.freeze(new Date('2014-01-01'));
});


afterAll(() => {
// Unlock Time
timekeeper.reset();
});

仅基于Date.now()的模拟的所有答案不会在任何地方都适用,因为一些包(例如moment.js)使用new Date()代替。

在这种情况下,基于MockDate的答案是我认为唯一真正正确的答案。如果你不想使用外部包,你可以直接在beforeAll中写:

  const DATE_TO_USE = new Date('2017-02-02T12:54:59.218Z');
// eslint-disable-next-line no-underscore-dangle
const _Date = Date;
const MockDate = (...args) => {
switch (args.length) {
case 0:
return DATE_TO_USE;
default:
return new _Date(...args);
}
};
MockDate.UTC = _Date.UTC;
MockDate.now = () => DATE_TO_USE.getTime();
MockDate.parse = _Date.parse;
MockDate.toString = _Date.toString;
MockDate.prototype = _Date.prototype;
global.Date = MockDate;

jest-date-mock是我自己写的一个完整的javascript模块,用来测试Date on jest。

import { advanceBy, advanceTo } from 'jest-date-mock';


test('usage', () => {
advanceTo(new Date(2018, 5, 27, 0, 0, 0)); // reset to date time.


const now = Date.now();


advanceBy(3000); // advance time 3 seconds
expect(+new Date() - now).toBe(3000);


advanceBy(-1000); // advance time -1 second
expect(+new Date() - now).toBe(2000);


clear();
Date.now(); // will got current timestamp
});

测试用例只使用3个api。

  • advanceBy(ms):提前日期的时间戳,单位为ms。
  • advanceTo([timestamp]):将日期重置为时间戳,默认为0。
  • Clear():关闭模拟系统。

我想提供一些替代方法。

如果你需要存根format()(它可以是地区和时区相关的!)

import moment from "moment";
...
jest.mock("moment");
...
const format = jest.fn(() => 'April 11, 2019')
moment.mockReturnValue({ format })

如果你只需要存根moment():

import moment from "moment";
...
jest.mock("moment");
...
const now = "moment(\"2019-04-11T09:44:57.299\")";
moment.mockReturnValue(now);

关于上面isDateToday函数的测试,我认为最简单的方法是根本不模拟moment

我想使用手动模拟,所以它可以在所有测试中使用。

// <rootDir>/__mocks__/moment.js
const moment = jest.requireActual('moment')


Date.now = jest.fn(() => 1558281600000) // 2019-05-20 00:00:00.000+08:00


module.exports = moment

目标是在组件呈现期间为测试目的而使用的任何地方,都使用固定日期模拟new Date()。如果您只想模拟new Date() fn,那么使用库将是一项开销。

想法是将全局日期存储到一个临时变量中,模拟全局日期,然后在使用后将temp重新分配给全局日期。

export const stubbifyDate = (mockedDate: Date) => {
/**
* Set Date to a new Variable
*/
const MockedRealDate = global.Date;


/**
*  Mock Real date with the date passed from the test
*/
(global.Date as any) = class extends MockedRealDate {
constructor() {
super()
return new MockedRealDate(mockedDate)
}
}


/**
* Reset global.Date to original Date (MockedRealDate) after every test
*/
afterEach(() => {
global.Date = MockedRealDate
})
}


Usage in your test would be like


import { stubbyifyDate } from './AboveMethodImplementedFile'


describe('<YourComponent />', () => {
it('renders and matches snapshot', () => {
const date = new Date('2019-02-18')
stubbifyDate(date)


const component = renderer.create(
<YourComponent data={}/>
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});




我只是想在这里插话,因为如果你只想在特定的套件中模拟Date对象,没有答案可以解决这个问题。

你可以使用每个套件jest文档的设置和拆卸方法来模拟它

/**
* Mocking Date for this test suite
*/
const globalDate = Date;


beforeAll(() => {
// Mocked Date: 2020-01-08
Date.now = jest.fn(() => new Date(Date.UTC(2020, 0, 8)).valueOf());
});


afterAll(() => {
global.Date = globalDate;
});

希望这能有所帮助!

对于那些想要在new Date对象上模拟方法的人,您可以执行以下操作:

beforeEach(() => {
jest.spyOn(Date.prototype, 'getDay').mockReturnValue(2);
jest.spyOn(Date.prototype, 'toISOString').mockReturnValue('2000-01-01T00:00:00.000Z');
});


afterEach(() => {
jest.restoreAllMocks()
});

这就是我如何嘲笑我的Date.now()方法来设置我的测试的年份为2010年

jest
.spyOn(global.Date, 'now')
.mockImplementationOnce(() => new Date(`2010`).valueOf());

你可以使用date-faker。允许您相对地更改当前日期:

import { dateFaker } from 'date-faker';
// or require if you wish: var { dateFaker } = require('date-faker');


// make current date to be tomorrow
dateFaker.add(1, 'day'); // 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond'.


// change using many units
dateFaker.add({ year: 1, month: -2, day: 3 });


// set specific date, type: Date or string
dateFaker.set('2019/01/24');


// reset
dateFaker.reset();

下面是一些针对不同用例的可读方法。我更喜欢使用间谍,而不是保存对原始对象的引用,因为原始对象可能会在其他代码中被意外覆盖。

一次性的嘲笑

jest
.spyOn(global.Date, 'now')
.mockImplementationOnce(() => Date.parse('2020-02-14'));

一些测试

let dateSpy;


beforeAll(() => {
dateSpy = jest
.spyOn(global.Date, 'now')
.mockImplementation(() => Date.parse('2020-02-14'));
});


afterAll(() => {
dateSpy.mockRestore();
});

从Jest 26开始,这可以用“modern”来实现。不需要安装任何第三方模块的假计时器:https://jestjs.io/blog/2020/05/05/jest-26#new-fake-timers

jest
.useFakeTimers()
.setSystemTime(new Date('2020-01-01'));

如果你想让假计时器为所有测试激活,你可以在你的配置中设置timers: 'modern': https://jestjs.io/docs/configuration#timers-string

编辑:截至Jest 27,现代假计时器是默认的,所以你可以将参数删除到useFakeTimers

这对我来说很管用:

const mockDate = new Date('14 Oct 1995')
global.Date = jest.fn().mockImplementation(() => mockDate) // mock Date "new" constructor
global.Date.now = jest.fn().mockReturnValue(mockDate.valueOf()) // mock Date.now

在我的情况下,我必须在测试前模拟整个Date和'now'函数:

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

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

Date.now = () => 1606348800;

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

我发现最好的方法就是用你正在使用的函数重写原型。

Date.prototype.getTimezoneOffset = function () {
return 456;
};


Date.prototype.getTime = function () {
return 123456;
};

@pranava-s-balugari的回复有所改善

  1. 它不影响new Date(something)
  2. 模拟日期可以更改。
  3. 它将工作日期。现在太
const DateOriginal = global.Date;


global.Date = class extends DateOriginal {
constructor(params) {
if (params) {
super(params)
} else if (global.Date.NOW === undefined) {
super()
} else {
super(global.Date.NOW)
}
}
static now () {
return new Date().getTime();
}
}


afterEach(() => {
global.Date.NOW = undefined;
})


afterAll(() => {
global.Date = DateOriginal;
});


describe('some test', () => {
afterEach(() => NOW = undefined);


it('some test', () => {
Date.NOW = '1999-12-31T23:59:59' // or whatever parameter you could pass to new Date([param]) to get the date you want




expect(new Date()).toEqual(new Date('1999-12-31T23:59:59'));
expect(new Date('2000-01-01')).toEqual(new Date('2000-01-01'));
expect(Date.now()).toBe(946681199000)


Date.NOW = '2020-01-01'


expect(new Date()).toEqual(new Date('2020-01-01'));
})
})

下面的测试存根Date在测试生命周期中返回一个常量。

如果你在你的项目中使用了new Date(),那么你可以在你的测试文件中模拟它,就像这样:

  beforeEach(async () => {
let time_now = Date.now();
const _GLOBAL: any = global;
_GLOBAL.Date = class {
public static now() {
return time_now;
}
};
}

现在无论你在测试文件中使用new Date(),它都会产生相同的时间戳。

注意:你可以用beforeAll替换beforeEach。而_GLOBAL只是一个满足typescript的代理变量。

完整的代码我尝试:

let time_now;
const realDate = Date;


describe("Stubbed Date", () => {
beforeAll(() => {
timeNow = Date.now();
const _GLOBAL: any = global;
_GLOBAL.Date = class {
public static now() {
return time_now;
}


constructor() {
return time_now;
}


public valueOf() {
return time_now;
}
};
});


afterAll(() => {
global.Date = realDate;
});


it("should give same timestamp", () => {
const date1 = Date.now();
const date2 = new Date();
expect(date1).toEqual(date2);
expect(date2).toEqual(time_now);
});
});

这对我很管用。

公认的答案很好

Date.now = jest.fn().mockReturnValue(new Date('2021-08-29T18:16:19+00:00'));

但是如果我们想在管道中运行单元测试,我们必须确保我们使用相同的时区。要做到这一点,我们必须模拟时区以及

jest.config.js

process.env.TZ = 'GMT';


module.exports = {
...
};

参见:时区的完整列表(列TZ数据库名)

我用的是moment + moment-timezone,这些都不适合我。

这工作:

jest.mock('moment', () => {
const moment = jest.requireActual('moment');
moment.now = () => +new Date('2022-01-18T12:33:37.000Z');
return moment;
});


我推荐sinonjs/fake-timers。它非常类似于jest提供的假计时器,但更加用户友好。

import FakeTimers from '@sinonjs/fake-timers';


const clock = FakeTimers.install()
clock.setSystemTime(new Date('2022-01-01'));


console.log(new Date()) // 2020-01-01T00:00:00.000Z

我正在使用一个外部库,为了让它工作,我必须在设置阶段运行以下代码:

Date.now = jest.fn(() => new Date(Date.UTC(2021, 2, 30)).valueOf());

我在jest.config.jssetupFilesAfterEnv道具中的setupTests.ts文件集中写了这个:

module.exports = {
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
};

要模拟toISOString,你可以这样做:

jest.spyOn(global.Date.prototype, 'toISOString').mockReturnValue('01-01-2001 00:00:00')