Today I Learned: Time travel in Jest

Today I Learned: Time travel in Jest

Today, I decided to work on test coverage for my package next-session, a session middleware for Next.js.

In one of the tasks, I needed to write a test to verify that the session store does not return the session if the session has expired.

jest is my testing framework.

server = await setUpServer((req, res) => {
  if (req.method === 'POST') {
    req.session.hello = 'world'; res.end();
  }
  if (req.method === 'GET') {
    res.end((req.session && req.session.hello) || '');
  }
}, { nextSession: { cookie: { maxAge: 5000 } } });

The function setUpServer simply get a server running with the specified next-session configuration, which, in this case, sets the cookie to expire in 5 seconds. POST will set the session and GET will retrieve the session (or return an empty string in case of no session).

I use supertest to make requests to the just created server.

const agent = request.agent(server);
await agent.post('/').then(() => agent.get('/').expect('wrold'));

The POST request first set the session. Then, I make a GET request and expect it to return the correct session value (req.session.hello === 'world').

It did!

Now, I needed to make another GET request five seconds later (Let's make it 10 just to be sure). I would expect an empty string since the session would have expired then.

I needed a way to make the code wait for five seconds.

(A)waiting is not happiness

They say "waiting is happiness", but it is not really when it comes to testing.

I thought of setTimeout. To make the code cleaner (and for my political hatred toward Callback), I decided to promisify setTimeout using Node.js's util.promisify, which I then await for.

await util.promisify(setTimeout)(10000);

However, I also have to jest.setTimeout(12000); // 12 seconds since the default config will fail the test if it runs passed 5 seconds.

Let's put it into action:

// jest.config.js
jest.setTimeout(12000);

// memory.test.js
await agent.post('/').then(() => agent.get('/').expect('world'));
await util.promisify(setTimeOut)(10000);
await agent.get('/').expect('');

Testing await setTimeout

It does the job. I mean, I can wait (because I'm such a patient person 😏), but what about the CI?

I began looking up for possible workarounds. The first thing that struck me was this thing called Timer Mocks (I mean, it has the word "Timer").

I called jest.useFakeTimers(); (and rewrite the whole setTimeout function without async function() since this does not work with the promisified setTimeout()).

The test went through in milliseconds, but it did not work. Well, it mocked setTimeout(), not the machine's clock.

Jest Mock Function to the rescue

jest.fn(implementation) allows me to execute a custom implementation when a function is called.

For example:

function beAMan() {
  confessToHer();
}
beAMan = jest.fn(() => walkAway());

if (seeHer) beAMan();

If I call beAMan(), instead of confessToHer(), I walkAway().

In the same manner, let's try it with Date.now().

//  Mock waiting for 10 second later for cookie to expire
const futureTime = new Date(Date.now() + 10000).valueOf();
global.Date.now = jest.fn(() => futureTime);

await agent.get('/').expect('');

global.Date.now.mockReset();

By doing so, when the check in next-session called Date.now(), it will receive the defined 10-second-later time. This will trick next-session into thinking that the session has actually expired.

I used global since Date.now() is called in another module. I also cleared the mock in global.Date.now.mockReset() to make sure it does not create any unintentional effect.

Yes, I am Doctor Strange now.

image