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

How to mock a constructor like new Date()

本文关键字:构造函数 Date 何模拟 模拟 new      更新时间:2023-10-31

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

有没有办法实际模拟构造函数的返回值?

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

要测试的示例函数...

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

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

从 jest 26 开始,您可以使用"现代"fakeTimers 实现(请参阅此处的文章(,它支持该方法jest.setSystemTime

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

请注意,'modern'将是 jest 版本 27 的默认实现。

有关setSystemTime,请参阅此处的文档。

更新:这个答案是jest < version 26最近的开玩笑版本看到这个答案的方法。

<小时 />

您可以使用jest.spyOn模拟像 new Date(( 这样的构造函数,如下所示:

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())
})

输出如下所示:

Normal:    1566424897579
Mocked:    1466424490000
Restored:  1566424897608

请参阅 GitHub 上的参考项目。

注意:如果您使用的是 TypeScript 并且会遇到编译错误,请Argument of type '() => Date' is not assignable to parameter of type '() => string'. Type 'Date' is not assignable to type 'string' 。在这种情况下,解决方法是使用 mockdate 库,该库可用于更改"现在"的时间。有关更多详细信息,请参阅此问题。

你可以使用 jasmine

的 spyOn(jest 是建立在 jasmine 上的(来模拟 Date 的 getDate 原型,如下所示:

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

SpyOn 也会在它自己之后进行清理,并且只在测试范围内持续。

尽管其他答案解决了这个问题,但我发现仅模拟Date的"无参数构造函数"行为同时保持Date的其他功能不变更自然且通常适用。例如,当 ISO 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();
}); 

您可以使用模拟函数覆盖 Date 构造函数,该函数返回您是一个构造的 Date 对象,其中包含您指定的日期值:

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)
})

您可以在此处测试示例:https://repl.it/@miluoshi5/jest-mock-date

您可以将 Date 构造函数替换为始终返回硬编码日期的内容,然后在完成后将其恢复正常状态。

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);

就我而言,我不得不在测试前模拟整个日期和"现在"函数:

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

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

Date.now = () => 1606348800;

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

你可以使用 date-faker 来模拟新的 Date(( 或 Date.now(( 返回的内容。

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();

只需这样做:

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)
}

它对我有用

如果您有多个日期(在多个测试中或在一次测试中多次(,则可能需要执行以下操作:

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;
  };
}

另请参阅此答案

<小时 />

原答案:

我刚刚写了一个开玩笑的测试,并且能够用global.Date = () => now存根new Date()

旧开玩笑版本的另一种方法可能是创建一个Date子类。

我只需要模拟我的用例的当前日期。被测代码使用new Date()和许多真正的Date方法。

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;

对于那些只需要在调用new Date()时有一个静态日期的人,可以用开玩笑来伪造系统时间:

/**
 * 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)
}

在之前:

now = Date.now()
setSystemTime(now)

毕竟:

jest.useRealTimers()

我正在使用 Typescript,我发现最简单的实现是执行以下操作:

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

然后使用new Date(date)进行测试

我遇到了一个错误,因为我的一个项目使用旧版本的笑话 - TypeError: setSystemTime is not available when not using modern timers 的@ThdK解决方案。所以我接下来做了,它对我有用

let dateNowSpy: jest.SpyInstance;
const today = new Date('2023-05-22').getTime();
describe('Some description', () => {
  beforeEach(() => {
    dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => today);
  });
  afterEach(() => {
    dateNowSpy.mockRestore();
  });
  test('some useful description', () => {
    /* Date.now() -> 2000-01-01 */
  });
});

这就是我现在正在做的事情,这正在工作并且不会弄乱我的方法的签名。

新日期.js

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

某模.js

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

一些模块测试.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());
  });
});