51

I'm using Enzyme with enzyme-to-json to do Jest snapshot testing of my React components. I'm testing shallow snapshots of a DateRange component that renders a display field with the current range (e.g. 5/20/2016 - 7/18/2016) and two DateInput components that allow selecting a Date value. This means that my snapshot contains the Dates I pass to the component both in the DateInput props and in a text representation it resolves itself. In my test I'm creating some fixed dates using new Date(1995, 4, 23).

When I run my test in different timezones, this produces different snapshots, because the Date(year, month, ...) constructor creates the date in the local timezone. E.g. use of new Date() produces this difference in snapshot between runs in my local timezone and on our CI server.

- value={1995-05-22T22:00:00.000Z}
+ value={1995-05-23T00:00:00.000Z}

I tried removing the timezone offset from the dates, but then the snapshot differed in the display field value, where the local timezone-dependent representation is used.

- value={5/20/2016 - 7/18/2016}
+ value={5/19/2016 - 7/17/2016}

How can I make my tests produce the same Dates in snapshots regardless of the timezone they're run in?

hon2a
  • 7,006
  • 5
  • 41
  • 55

8 Answers8

81

I struggled with this for hours/days and only this worked for me:

1) In your test:

Date.now = jest.fn(() => new Date(Date.UTC(2017, 7, 9, 8)).valueOf())

2) Then change the TZ env var before running your tests. So the script in my package.json:

  • (Mac & Linux only)

    "test": "TZ=America/New_York react-scripts test --env=jsdom",
    
  • (Windows)

    "test": "set TZ=America/New_York && react-scripts test --env=jsdom",
    
Devid
  • 1,823
  • 4
  • 29
  • 48
morgs32
  • 1,439
  • 3
  • 17
  • 26
  • 15
    This (IMO) should be the accepted answer. Specifically, step #2 was sufficient for me. I just added a quick `TZ` environment variable in my test script declaration, and it worked like a charm. I didn't even have to mock out any Date function. – John T Nov 02 '17 at 15:12
  • Cool, I'll mark it as accepted then. As it got posted almost a year after my original troubles, I never saw it till now :) – hon2a Jun 18 '18 at 19:27
  • 3
    Here's the whole line I'm now using `"test": "TZ=America/New_York jest",`. That's all it took. Thank you! – SudoPlz Sep 20 '18 at 17:58
  • 2
    Specifying `TZ=...` also worked for me! For reference http://www.timezoneconverter.com/cgi-bin/tzc has a list of timezone values that should all work. – AuthorOfTheSurf Nov 21 '18 at 02:38
  • I get the error 'TZ is not recognized as an internal or external command,'. Do I have to install something ? – Devid Nov 22 '18 at 11:20
  • 1
    Ok I figured it out and edited the answer with the solution. – Devid Nov 22 '18 at 12:29
  • I wish I could upvote this more. Wasted an entire day on this issue before finding this post. For context I used it as part of a Jenkins pipeline `"CI=true TZ=America/Los_Angeles npm test"` – adam.shaleen Feb 19 '19 at 16:14
  • I get : "react-scripts" is not found :( I tried "test": "set TZ=UTC jest". But this didn't work either. – Oliver Watkins Jul 30 '19 at 14:14
  • 2
    You save my day ! @Devid For all env (install cross-env package before) : `"test": "cross-env TZ=America/New_York && react-scripts test --env=jsdom"` – fabienbranchel Apr 07 '20 at 10:22
5

I ended up with a solution comprised of two parts.

  1. Never create Date objects in tests in timezone-dependent manner. If you don't want to use timestamps directly to have readable test code, use Date.UTC, e.g.

    new Date(Date.UTC(1995, 4, 23))
    
  2. Mock the date formatter used to turn Dates into display values, so that it returns a timezone-independent representation, e.g. use Date::toISOString(). Fortunately this was easy in my case, as I just needed to mock the formatDate function in my localization module. It might be harder if the component is somehow turning Dates into strings on its own.

Before I arrived at the above solution, I tried to somehow change how the snapshots are created. It was ugly, because enzyme-to-json saves a local copy of toISOString(), so I had to use _.cloneDeepWith and modify all the Dates. It didn't work out for me anyway, because my tests also contained cases of Date creation from timestamps (the component is quite a bit more complicated than I described above) and interactions between those and the dates I was creating in the tests explicitly. So I first had to make sure all my date definitions were referring to the same timezone and the rest followed.


Update (11/3/2017): When I checked enzyme-to-json recently, I haven't been able to find the local saving of toISOString(), so maybe that's no longer an issue and it could be mocked. I haven't been able to find it in history either though, so maybe I just incorrectly noted which library did it. Test at your own peril :)

hon2a
  • 7,006
  • 5
  • 41
  • 55
4

I did this by using timezone-mock, it internally replaces the global Date object and it's the easiest solution I could find.

The package supports a few test timezones.

import timezoneMock from 'timezone-mock';

describe('when in PT timezone', () => {
  beforeAll(() => {
    timezoneMock.register('US/Pacific');
  });

  afterAll(() => {
    timezoneMock.unregister();
  });

  // ...

https://www.npmjs.com/package/timezone-mock

miguelr
  • 1,334
  • 14
  • 21
1

I ended up getting around this by mocking the toLocaleString (or whatever toString method you are using) prototype. Using sinon I did:

var toLocaleString;

beforeAll(() => {
    toLocaleString = sinon.stub(Date.prototype, 'toLocaleString', () => 'fake time')
})

afterAll(() => {
    toLocaleString.restore()
})

This way if you are generating strings straight from a Date object, you're still OK.

Matt
  • 4,029
  • 3
  • 20
  • 37
  • Unfortunately, as I describe in my answer, `enzyme-to-json` saves and uses a local copy of `Date.toISOString`, so the stub doesn't affect the result. – hon2a Jan 28 '17 at 16:40
  • @morgs32 At the time of writing, I distinctly remember looking at code doing just that. But now I can't find it anywhere, so I can't be sure that it wasn't in some library used as dependency or something like that. – hon2a Oct 26 '17 at 21:41
1

2020 solution that works for me

beforeEach(() => {
        jest.useFakeTimers('modern');
        jest.setSystemTime(Date.parse(FIXED_SYSTEM_TIME));
});

afterEach(() => {
        jest.useRealTimers();
});
Dante Nuñez
  • 439
  • 4
  • 7
0

If you're using new Date() constructor instead of Date.now you can do like below:

const RealDate = Date;

beforeEach(() => {
  // @ts-ignore
  global.Date = class extends RealDate {
    constructor() {
      super();
      return new RealDate("2016");
    }
  };
})
afterEach(() => {
  global.Date = RealDate;
});

This issue is a must visit if you're here.

Black Mamba
  • 13,632
  • 6
  • 82
  • 105
0

Adding TZ=UTC to my .env file solved the issue for me.

lucas
  • 1,105
  • 8
  • 14
0

A simple fact can make it easy.

Just use :

new Date('some string'). 

This will always give an invalid date and no matter which machine, it will always be invalid date.

cheers.