⏰ 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

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

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 immediatelyjest.advanceTimersByTime(ms)— Moves time forward by specific millisecondsjest.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

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