nicolo-ribaudo / jest-light-runner

A Jest runner that runs tests directly in bare Node.js, without virtualizing the environment.
MIT License
232 stars 24 forks source link

jest.setSystemTime is not working, but mockdate is #74

Closed alucryd closed 1 year ago

alucryd commented 1 year ago

Hi there, I'm trying to use the fake timers from jest, but I can't get them to work. I'm using jest-light-runner with esmock, and my dates are handled by dayjs.

Non-working snippet:

const now = dayjs();

beforeAll(async () => {
  await db.raw(`ATTACH DATABASE ":memory:" AS ${Customer.schema}`);
  await db.migrate.latest();
  jest.useFakeTimers();
  jest.setSystemTime(now);
});

Resulting in:

          OptIn {
    -       "effectiveAt": "2023-06-30T08:47:48.437Z",
    +       "effectiveAt": "2023-06-30T08:47:48.463Z",
            "name": "text",
            "value": true,
          },

Working snippet:

const now = dayjs();

beforeAll(async () => {
  await db.raw(`ATTACH DATABASE ":memory:" AS ${Customer.schema}`);
  await db.migrate.latest();
  MockDate.set(now);
});

Am I doing something wrong? I also tried legacy timers in jest to no avail.

SimenB commented 1 year ago

What version are you on? Fake timers support was only published a few weeks ago: https://github.com/nicolo-ribaudo/jest-light-runner/releases/tag/v0.5.0

alucryd commented 1 year ago

I am on 0.5.0, here are my dev deps:

"devDependencies": {
    "@babel/core": "^7.22.5",
    "@babel/eslint-parser": "^7.22.5",
    "@babel/plugin-syntax-import-assertions": "^7.22.5",
    "babel-jest": "^29.5.0",
    "better-sqlite3": "^8.4.0",
    "c8": "^8.0.0",
    "clinic": "^12.1.0",
    "esmock": "^2.3.1",
    "healthier": "^6.3.0",
    "jest": "^29.5.0",
    "jest-junit": "^16.0.0",
    "jest-light-runner": "^0.5.0",
    "mockdate": "^3.0.5",
    "nodemon": "^2.0.22",
    "prettier": "^2.8.8",
    "supertest": "^6.3.3"
  },
Zikoat commented 1 year ago

setSystemTime takes a Date object, not a Dayjs object. The correct usage is this.

  jest.useFakeTimers().setSystemTime(dayjs().toDate());

You would have caught this error if you would have used TypeScript:

image

Me and 2xic (feature author) have used the features released in 0.5.0 in our own larger codebase for 6 months now. Below are some code snippets and functions that have served us well.

jest.setup.after.env.js

const dayjs = require('dayjs');

expect.addSnapshotSerializer({
  test: (arg) => dayjs.isDayjs(arg),
  print: (val) => {
    const content = val.isSame(val.startOf('day')) ? val.format('YYYY-MM-DD') : val.toISOString();

    return '"' + content + '"';
  },
});

setMockTime.ts

import dayjs, { Dayjs } from 'dayjs';

/**
 * Be sure to call jest.useRealTimers() in afterEach when using this, or the pipeline will time out after 1 hour.
 */
export function setMockTime(dateTime: string | Dayjs): Dayjs {
  const dayjsObject = dayjs(dateTime, { utc: true }).tz('Europe/Oslo'); // your timezone

  // standard validation of dayjs objects
  try {
    dayjsObject.toISOString();
  } catch (e) {
    throw new Error(`Invalid date string: "${dateTime}"`);
  }

  jest.useFakeTimers().setSystemTime(dayjsObject.toDate());

  return dayjsObject;
}

setMockTime.test.ts

import dayjs from 'dayjs';
import { setMockTime } from './setMockTime';

describe('setMockTime', () => {
  afterEach(() => {
    jest.useRealTimers();
  });

  it('should set the time to a constant value', async () => {
    setMockTime('2023-01-01 12:00');

    expect(dayjs()).toMatchInlineSnapshot(`"2023-01-01T12:00:00.000Z"`);
  });

  it('should throw error if we try to set the time to an invalid time string', () => {
    expect(() => setMockTime('2023-01--01 12:00:00')).toThrowErrorMatchingInlineSnapshot(
      `"Invalid date string: "2023-01--01 12:00:00""`,
    );
  });
});
Zikoat commented 1 year ago

This can be closed if the fix works for @alucryd

nicolo-ribaudo commented 1 year ago

Thank you! I'll reopen if the fix does not work.

SimenB commented 1 year ago

should probably be a runtime error in Jest...

alucryd commented 1 year ago

@nicolo-ribaudo Using a Date object does indeed work, thanks! However I'm now running into another issue, the timer logic works fine for a single test file, however if I try to put the following block in multiple files, subsequent calls to jest.useFakeTimers() will completely freeze the tests, only the first file will run, then the terminal hangs indefinitely.

now = dayjs();

beforeAll(async () => {
  jest.useFakeTimers().setSystemTime(now.toDate());
});

afterAll(async () => {
  jest.useRealTimers();
});
Zikoat commented 1 year ago

Yes, i have also seen the bug you are describing. My current workound is to be sure to call jest.useRealTimers() in afterEach when i am using fake timers. I hinted at this in the docstring of my setMockTime function above. We usually don't mock the time in every test, but only when the test relies on the time to pass. Most of your tests should work without mocking the time.

@alucryd Could you create a new issue with a minimal reproducible example and instructions of how to execute this bug? It is better to fix this in a new issue than to reuse this closed issue.