asynchronous testing
unittest
RuntimeError
Python testing
async programming

Asynchronous testing with unittest RuntimeError

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When asynchronous tests fail under Python's unittest, the error is often not in the coroutine logic itself. The real problem is usually event-loop misuse, especially code that tries to create or drive a loop manually in a context where the test framework already expects to manage async execution.

Why the RuntimeError Happens

Common failure messages include:

  • 'RuntimeError: This event loop is already running'
  • 'RuntimeError: no running event loop'

These usually mean the test is using the wrong strategy for async code. For example, calling asyncio.run() from the wrong place or mixing sync and async test styles carelessly can produce loop conflicts.

The Modern Fix: IsolatedAsyncioTestCase

For modern Python, the cleanest unittest solution is unittest.IsolatedAsyncioTestCase. It gives each async test a managed loop and avoids manual event-loop plumbing.

python
1import asyncio
2import unittest
3
4
5async def fetch_value():
6    await asyncio.sleep(0.01)
7    return 42
8
9
10class TestFetchValue(unittest.IsolatedAsyncioTestCase):
11    async def test_fetch_value(self):
12        result = await fetch_value()
13        self.assertEqual(result, 42)
14
15
16if __name__ == "__main__":
17    unittest.main()

This is the approach to prefer when you are testing plain async functions or async service objects.

What Not to Do

A frequent anti-pattern is driving async code manually from an ordinary TestCase with asyncio.run():

python
1import asyncio
2import unittest
3
4
5async def fetch_value():
6    return 42
7
8
9class BadTest(unittest.TestCase):
10    def test_fetch_value(self):
11        result = asyncio.run(fetch_value())
12        self.assertEqual(result, 42)

That can appear to work in simple scripts, but it becomes fragile when the surrounding environment is already using an event loop.

Async Setup and Teardown

IsolatedAsyncioTestCase also supports async lifecycle hooks, which is useful for clients, queues, and other async resources.

python
1import unittest
2
3
4class TestService(unittest.IsolatedAsyncioTestCase):
5    async def asyncSetUp(self):
6        self.items = []
7
8    async def test_append(self):
9        self.items.append("ok")
10        self.assertEqual(self.items, ["ok"])
11
12    async def asyncTearDown(self):
13        self.items.clear()

This keeps async resource management in the same style as the test body.

Older Python Needs Manual Loop Management

If you are stuck on an older Python version, the fallback is a dedicated loop per test.

python
1import asyncio
2import unittest
3
4
5async def fetch_value():
6    await asyncio.sleep(0.01)
7    return 42
8
9
10class LegacyTest(unittest.TestCase):
11    def test_fetch_value(self):
12        loop = asyncio.new_event_loop()
13        try:
14            result = loop.run_until_complete(fetch_value())
15            self.assertEqual(result, 42)
16        finally:
17            loop.close()

It works, but it is more error-prone than the isolated async test case model.

Mock Async Dependencies Correctly

A lot of async-test confusion comes from mocks rather than loops. If your code awaits a dependency, use AsyncMock so the fake behaves like an awaitable.

python
from unittest.mock import AsyncMock

service = AsyncMock(return_value="ok")

That avoids a whole class of misleading failures where the test technically runs but the mocked object does not behave like a coroutine.

If your project already uses another async-aware test runner, keep the loop-management strategy consistent across the suite. Mixing incompatible async styles is one of the easiest ways to produce flaky runtime errors that appear and disappear depending on execution order.

Common Pitfalls

  • Calling asyncio.run() inside tests that already have loop management.
  • Using plain TestCase when IsolatedAsyncioTestCase is available.
  • Forgetting to await a coroutine and accidentally asserting on the coroutine object itself.
  • Reusing loops or async resources across tests and leaking state.
  • Mocking awaited dependencies with ordinary mocks instead of AsyncMock.

Summary

  • Most async unittest runtime errors come from event-loop misuse, not business logic.
  • Prefer unittest.IsolatedAsyncioTestCase for modern async tests.
  • Avoid nesting asyncio.run() in environments that already manage a loop.
  • On older Python, create and close a dedicated loop per test.
  • Use AsyncMock for awaited dependencies so tests behave like real async code.

Course illustration
Course illustration

All Rights Reserved.