How to correctly mock Moment.js/Dates in Jest

How to correctly mock Moment.js/Dates in Jest

Times and dates are infamously hard to correctly implement in code. This makes testing date and time code correctly important. Testing allows for reasoning around logic in code and also to allow catching edge cases or errors before they impact users.

A common mistake when testing date and time code is to not set the current time to a static time. If code in the UI renders today’s date and is tested properly, that test that only works until the current time changes too much. Javascript exposes the built-in Date object which allows for retrieving the current time through construction with no arguments or a call to the now() property.

Moment.js is a popular front-end date manipulation library that is commonly used to manipulate, load, format, and shift time. It uses an empty constructor to get the current time. Jest is often used in conjunction with Moment and React applications. Additionally, Jest snapshot testing introduces new dependencies on date and time that are important to consider. Below is an example problematic component that renders the current day:

import React from 'react';
import moment from 'moment;
export function TodayIntro(name) {
  return <h1>hi {name}, today is {moment().format('MMM D')}</h1>;
}

An initial test for the TodayIntro component could look like:

// Fails when today is Jan 24 :(
expect(shallow(<TodayIntro name="iain" />).text())
  .toEqual('hi iain, today is Jan 23');

However, this test will fail on any day that is not Jan 23rd. A solution to this is to override Javascript’s date function to return a known date to work against when writing tests.

This code overrides the Date constructor to set a static “current” date:

// Mock date constructor for Moment (recommended to use a library)
const nowString = '2018-02-02T20:20:20';
const MockDate = (lastDate) => (...args) =>
  new lastDate(...(args.length ? args : [nowString]);
global.Date = jest.fn(MockDate(global.Date));
expect(
  shallow(<TodayIntro name="iain" />).text()
).toEqual('hi iain, today is Feb 2');
// Restore original object
global.Date.mockRestore();

An ineffective solution is to do the date math yourself against the current time the test is run. This is an ineffective test because you’re running the same code you’re testing to test the return value. For instance, if testing by comparing formatted dates through moment, one would not catch if the moment formatting code changes MMM to JAN instead of Jan .

expect(
  shallow(<TodayIntro name="iain" />).text()
).toEqual(`hi iain, today is ${moment.format('MMM D')}`);

Ways to set a static time and timezone for Jest/JS

  1. Use a library to mock out Date object to return a static date and timezone (we’d recommend MockDate for simple cases, but read on for a breakdown of the alternatives)
  2. Mock moment().format() to return a static string
  3. Mock the Date constructor and now() function to return a static time
// Mocking out the Date.now() function (not used by moment)
const realDateNow = Date.now;
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2019-01-01'));
// Test code here
Date.now.mockRestore();

// Mocking out Date constructor object (used by moment)
const nowString = '2018-02-02T20:20:20';
// Mock out the date object to return a mock null constructor
const MockDate = (lastDate) => (...args) =>
  new lastDate(...(args.length ? args : [nowString]);
global.Date = jest.fn(MockDate(global.Date));
// Test code here
global.Date.mockRestore();

// Using mockdate library
import MockDate from 'mockdate';
MockDate.set('2018-1-1');
// Test code here
MockDate.reset();

// Using sinon library
import sinon from 'sinon';
// Mocking with Sinon (also mocks timers)
const clock = sinon.useFakeTimers(new Date('2018-01-01'));
// Restoring
clock.restore();

Using a library in this case is preferable because these libraries are well tested, do not introduce boilerplate code and handle transparently both cases dates can be created (Date.now() vs new Date() etc.). Additionally, using a library allows for easily following test code and setting a specific time per test which allows for better testing practices.

  • MockDate provides further functionality for time zones and is easy to use
  • sinon provides Date and timer (setTimeout etc.) mocks
  • Manually setting the mock can be useful in limited environments, however, can become rather complicated
  • jasmine (not included in jest), comes with a jasmine.clock()

The examples below use MockDate, which only focuses on mocking the Date object simply and handles testing time zone offsets as well for testing local time zone conversion.

import ShallowRenderer from 'react-test-renderer/shallow';
import {TodayIntro} from './TodayIntro';
describe('TodayIntro', () => {
  it('should render a hello message with a date', () => {
    MockDate.set('2018-01-01');
    
    const renderer = new ShallowRenderer();
    renderer.render(<TodayIntro name="test" />);
    
    const result = renderer.getRenderOutput();
    expect(result.type).toBe('h1');
    expect(result.children).toBe(['hello test, today is Jan 1']);
    
    MockDate.reset();
  });
});

A snapshot test, as well, is simple to test with mocked dates:

import renderer from 'react-test-renderer';
import {TodayIntro} from './TodayIntro';
describe('TodayIntro', () => {
  afterAll(() => MockDate.reset());
  it('should render a hello message with a date', () => {
    MockDate.set('2018-01-01');
    const tree = renderer.create(<TodayIntro name="test" />).toJSON();
    expect(tree).toMatchSnapshot();
  });
});

Since enzyme is an awesome library, an enzyme shallow example:

import {shallow} from 'enzyme';
import {TodayIntro} from './TodayIntro';
describe('TodayIntro', () => {
  afterAll(() => MockDate.reset());
  it('should render a hello message with a date', () => {
    MockDate.set('2018-01-01');
    const text = shallow(<TodayIntro name="test" />).text();
    expect(text).toEqual('hello test, today is Jan 1');
  });
});

How to (better) test date logic

Dates have a lot of edge cases and logic behind them. When testing dates, make sure to cover edge cases and not just set one specific date to test and move on. Dates can also vary according to locale and time zone.

Properly testing dates require reasoning around edge cases that could occur and writing tests to ensure those edge cases behave as expected and that future changes to code or libraries used in your application don’t break those assumptions. Additionally, adding code to set the current date and time to a static date and time across all test code may be easier, but prevents good reasoning around testing Dates and hides test assumptions in library code.

Here are a few incorrect and often implicit assumptions about dates:

  1. Clients all exist within one time zone and daylight saving time
  2. All clients exist within the developer’s time zone
  3. The length of a Month name is relatively similar
  4. Server clocks are always correct
  5. The server knows the client’s timezone/time settings
// An example test that is more brittle than it appears
function formatUserDate(user) {
  user.localCreated = moment().utc(user.created).local();
}
class UsersList extends Component {
  componentDidMount() {
    this.setState({users:
      loadUsers(users).map((user) => formatUserDate)
    });
  }
  // Rendering implementation here
}
// Test
it('should load users correctly', () => {
  jest.spyOn(Datastore, 'loadUsers').mockImplementation(() => 
    [{name: 'test', created: '2019-01-02T20:00:00'}]
  ))});
  expect(mount(<UsersList />)).toMatchSnapshot();
});

This test assumes the server is always in the correct timezone and that timezone is set correctly. Instead, set the timezone and make sure the date matches the local timezone correctly.

it('should load users correctly in TZ 120 min offset', () => {
  MockDate.set('2020-02-02', 120);
  jest.spyOn(Datastore, 'loadUsers').mockImplementation(() => 
    [{name: 'test', created: '2019-01-02T20:00:00'}]
  ))});
  expect(mount(<UsersList />)).toMatchSnapshot();
});
it('should load users correctly in TZ 10 min offset', () => {
  MockDate.set('2020-02-02', 10);
  jest.spyOn(Datastore, 'loadUsers').mockImplementation(() => 
    [{name: 'test', created: '2019-01-02T18:10:00'}]
  ))});
  expect(mount(<UsersList />)).toMatchSnapshot();
});

It is important to ensure that when tests access the current time the “current time” is set to a static value. If the value is dynamic, either tests eventually break or a test is testing against dynamic values. Dynamic values are not effective at testing behavior since a bug will not be exposed by comparing the return value of two functions that are the same as compared to comparing to a static value that doesn’t change as the code is modified.

Looking ahead: Date time storage and design

Having a requirement to add tests to a code base doesn’t necessarily provide any value unless those tests are reviewed, run, and reasoned about just as strictly as running code.

Date and time logic introduces a large set of possibilities in terms of behavior and output, making a strong incentive to test effectively for date and time. Beyond testing, acknowledging and keeping relevant data along with a strategy to synchronize and store date times consistently across systems early on both helps testing and makes for a better user experience.

These tips and approaches apply to more than just Javascript & Jest testing for dates and times. They also work in a NodeJS context and in a general sense around key things to test for in systems that handle date and time in general. In many cases, storing time on the server in UTC (Universal coordinated time) then converting to the local time zone based on client / browser settings is ideal. If the client is inaccessible, storing both the UTC time and user’s actual timezone is an effective way to consistently treat dates and times.