How do you test promises, code that updates after awaiting, and time-dependent logic (debounce, setTimeout)?
Handling asynchrony without arbitrary sleeps, and controlling time deterministically instead of waiting for it.
The golden rule for async tests: never sleep for a fixed duration and hope — that's slow and flaky. Instead, AWAIT the thing or POLL for the expected state. For a promise-returning function, make the test async and await it (or use resolves/rejects matchers: await expect(load()).resolves.toEqual(...)). For UI that updates AFTER async work (fetch resolves, then the DOM changes), use Testing Library's findBy* (which retries until the element appears or times out) or waitFor(() => expect(...)) which re-runs the callback until it passes. That polls for the OUTCOME rather than guessing a delay, so it's both fast (resolves as soon as it's true) and robust (won't fail on a slightly slow machine). For TIME-dependent logic — debounce, throttle, setTimeout/setInterval, polling — don't wait real seconds; take control of the clock with FAKE TIMERS (jest.useFakeTimers()). Then you ADVANCE time deterministically: jest.advanceTimersByTime(300) fast-forwards 300ms so a debounced callback fires instantly and predictably, and the test runs in milliseconds with zero flakiness. Remember to restore real timers afterward (jest.useRealTimers() in afterEach) so you don't leak the fake clock into other tests, and note that when combining fake timers with user-event you configure user-event with the fake-timer advance function. Two classic mistakes to avoid: (1) not awaiting — the test finishes and passes before the assertion runs, giving false green (or a warning about state updates after the test); (2) asserting immediately after triggering async work without waiting, so you check the DOM before it has updated. The pattern to internalize: control what you can make deterministic (time — fake it), and for genuinely async outcomes, poll for the result instead of racing it.
Testing fetch-driven UI, debounced handlers, timeouts, or anything that resolves later.
// 1) Wait for an async OUTCOME (no arbitrary sleep)
test('shows users after load', async () => {
render(<UserList />);
expect(await screen.findByText('Ada')).toBeInTheDocument(); // retries until present
});
// 2) Control time for debounce/timers instead of waiting for it
test('debounces search by 300ms', () => {
jest.useFakeTimers();
const cb = jest.fn();
const search = debounce(cb, 300);
search('a'); search('ab');
jest.advanceTimersByTime(300); // fast-forward, no real delay
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenCalledWith('ab');
jest.useRealTimers();
});