Lesson 9 of 9 · 18m
Testing Async, Time, and Events
The untestable-triple — clocks, randomness, and delays — plus event-driven code and the patterns that make async testable.
Async code is hard to test because it's nondeterministic by nature — timing, ordering, timeouts. The fix isn't a better assertion; it's injecting the nondeterminism so the test controls it.
The untestable triple
def process_order(order):
if time.time() - order.created_at > 86400: # untestable — real clock
order.status = "expired"
send_email() # untestable — real side effect
time.sleep(2) # untestable — real delayEvery one of those is a test smell. Inject the clock, the side effect, and the delay:
def process_order(order, clock=time.time, send=send_email):
if clock() - order.created_at > 86400:
order.status = "expired"
send()The test passes a fake clock (lambda: fixed_time) and a recording fake — you assert on the observable outcome, not the timing.
Testing timeouts and retries
A retry loop that takes 5 real seconds per test is a slow suite. Inject the retry policy or the delay:
def fetch_with_retry(url, max_retries=3, delay=lambda: time.sleep(1)):
for i in range(max_retries):
try: return requests.get(url)
except: delay()Test with delay=lambda: None and max_retries=0 — the timeout behavior is now testable without waiting.
Event-driven code
An event loop isn't testable as "fire the event and check" — the handler is async, so the test needs to wait for the side effect. Use asyncio.wait_for with a timeout (fails the test if it never arrives) or a synchronous queue consumer for tests:
async def test_handler():
event.set()
await asyncio.wait_for(result_future, timeout=2)
assert result.done()Field notes
freezegun/time-machinepatch the clock globally — useful for integration tests but prefer dependency injection for unit tests (the test shouldn't depend on global patching).asyncio.sleep(0)in a test yields control without real time — use it to let the event loop process pending tasks.- An async test that doesn't await the assertion races the event loop — always
awaitthe side effect you expect.