Testing and Debugging
Async bugs are often timing bugs, which means they do not always reproduce the same way twice. That is why async testing needs timeout guards, task-leak checks, and debug mode rather than only happy-path assertions.
Quick takeaway: do not just assert the final value. Use timeout guards to fail hangs, enable debug mode to surface misuse, and check that no extra tasks survive when the test is done.
Debugging Flow
First Guardrail to Add
import asyncio
async def fetch_with_guard() -> str:
async with asyncio.timeout(0.5):
await asyncio.sleep(0.1)
return "ok"
def main() -> None:
runner = asyncio.Runner(debug=True)
with runner:
print(runner.run(fetch_with_guard()))
if __name__ == "__main__":
main()`asyncio.timeout()` turns hangs into failures, while `Runner(debug=True)` or `asyncio.run(..., debug=True)` enables additional debug checks and warnings.
Simple Task-Leak Check
import asyncio
async def assert_no_extra_tasks() -> None:
current = asyncio.current_task()
leaked = {
task
for task in asyncio.all_tasks()
if task is not current and not task.done()
}
if leaked:
raise AssertionError(f"leaked tasks: {leaked}")What Debug Mode Helps Surface
- forgotten awaits
- wrong thread usage of loop APIs
- slow callbacks and selector operations
- unclosed transports or resource warnings
Checklist
Every test gets a timeout
Hangs are worse than ordinary failures. Always bound execution time.
Verify cleanup paths
Cancellation should leave queues, semaphores, and background tasks in a clean state.
Use debug mode intentionally
Debug mode is excellent for local reproduction and CI diagnosis of subtle async issues.
Inspect pending tasks
Checking for leftover tasks catches leaks early.