StoryNovember 10, 20253 min read

⏰ Mocking Timers and Async Behavior in Jest — Expense Tracker Edition

Our Expense Tracker has been through a lot already.


⏰ Mocking Timers and Async Behavior in Jest — Expense Tracker Edition

Designed by Auther using Figma

Our Expense Tracker has been through a lot already.

It can calculate totals,

handle errors,

mock APIs,

and even spy on its own functions.

But there’s one thing it still cannot control yet — time.

In programming, time is unpredictable.

Timers, delays, and async functions make tests harder to manage.

Sometimes your test finishes before your function does.

Other times it just hangs there… waiting forever.

In this post, we will teach our Expense Tracker how to pause, fast-forward, and even skip time — all with Jest’s timer mocks.

💡 Why Time Is a Problem in Tests

Image generated by ChatGPT

Imagine our app adds a new feature:

Every day at 6 PM, it reminds you to log your expenses.

Sounds simple, right?

But how do you test that?

You cannot sit for 24 hours waiting to see if your test reminder triggers correctly.

That’s where Jest fake timers come in.

They let you simulate the passage of time instantly inside your tests.

⚙️ Step 1 — Create a Reminder Function

Let’s add a new file: expenseReminder.js

exports.scheduleReminder = (callback, delay) => {
  console.log('Reminder scheduled for', delay, 'ms');
  setTimeout(() => {
    callback('Time to log your expenses!');
  }, delay);
};

This function accepts a callback and a delay.

It waits for that delay, then executes the callback.

Simple in code,

but a pain to test without waiting in real time.

🧪 Step 2 — Write a Test Without Mocks (the painful way)

📁 expenseReminder.test.js

const { scheduleReminder } = require('./expenseReminder');

test('should call callback after delay (real timer)', done => {
  function mockCallback(message) {
    expect(message).toBe('Time to log your expenses!');
    done();
  }

  scheduleReminder(mockCallback, 2000);
});

This test works

but it literally takes 2 seconds to run.

Imagine hundreds of these,

your test suite would crawl.

⚡ Step 3 — Use Jest Fake Timers

Let’s fix it with one line:

jest.useFakeTimers();

Now we can jump through time instantly.

📁 expenseReminder.test.js (updated)

const { scheduleReminder } = require('./expenseReminder');

test('should call callback after delay using fake timers', () => {
  jest.useFakeTimers();
  const mockCallback = jest.fn();

  scheduleReminder(mockCallback, 2000);

  // Fast-forward all timers
  jest.runAllTimers();

  expect(mockCallback).toHaveBeenCalledWith('Time to log your expenses!');
});

Result:

Instant test.

No waiting.

You just fast-forwarded 2 seconds of time in a millisecond.

🧠 Step 4 — Understanding Jest Timer Methods

Jest gives you three main timer controls:

  • jest.runAllTimers() — Runs every scheduled timer immediately
  • jest.advanceTimersByTime(ms) — Moves time forward by specific milliseconds
  • jest.clearAllTimers() Clears all pending timers (useful for cleanups)

🕵️ Step 5 — Simulating Multiple Reminders

Let’s test multiple timers firing at different delays.

test('should handle multiple scheduled reminders', () => {
  jest.useFakeTimers();
  const callback = jest.fn();

  scheduleReminder(callback, 1000);
  scheduleReminder(callback, 3000);

  // Fast-forward time
  jest.advanceTimersByTime(1000);
  expect(callback).toHaveBeenCalledTimes(1);

  jest.advanceTimersByTime(2000);
  expect(callback).toHaveBeenCalledTimes(2);
});

Now your tests are literally controlling time.

Feels good, right?

🧩 Step 6 — Handling Async + Timers Together

Sometimes your async function also includes a timeout.

Here is how Jest can handle both.

📁 expenseReminderAsync.js

exports.scheduleAsyncReminder = async (callback) => {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  callback('Reminder after async wait');
};

📁 expenseReminderAsync.test.js

const { scheduleAsyncReminder } = require('./expenseReminderAsync');

test('should trigger reminder after async delay', async () => {
  jest.useFakeTimers();
  const callback = jest.fn();

  const promise = scheduleAsyncReminder(callback);

  jest.runAllTimers();
  await promise;

  expect(callback).toHaveBeenCalledWith('Reminder after async wait');
});

💡 Notice how we used await promise even after running timers

This ensures async tasks inside setTimeout also resolve before verification.

🧠 Real-World Tip

When testing async code that mixes setTimeout and await,

always use both jest.runAllTimers() and await to flush pending microtasks.

That small habit will save you hours of debugging weird “test finished too early” issues.

✅ Summary

Image generated by ChatGPT

Today,

our Expense Tracker learned how to bend time.

No more waiting around for timers to finish or async functions to behave.

We just fast-forwarded life itself.

What a day,

right?

Here’s what we pulled off:

✨ We found out why time is such a pain in tests.

⏰ We used jest.useFakeTimers() to take control of it.

⚙️ We learned how jest.runAllTimers() and jest.advanceTimersByTime() can skip delays instantly.

💫 And we made async tests fly without waiting a single second.

Next stop → we go even deeper into mocking modules and dependencies, where things start feeling like testing magic.

💬 Comment what part confused you — I will simplify it in the next post.

Thanks for reading.

If this added value, follow me for more clear and practical posts.
— Alkesh Jethava