How to Resolve "RuntimeWarning: coroutine was never awaited" in Pytest Async Tests (Test Passing When It Should Fail)

Asynchronous programming in Python has become increasingly popular, especially with frameworks like FastAPI, aiohttp, and libraries such as asyncio. However, testing async code introduces unique challenges. One common frustration is the RuntimeWarning: coroutine was never awaited, which often hides a deeper issue: tests passing when they should fail.

This warning isn’t just a harmless log message—it signals that your test isn’t executing the async code properly. If ignored, it can lead to false positives, where your test suite claims success even when critical bugs exist. In this guide, we’ll demystify this warning, explore why tests fail silently, and provide actionable solutions to ensure your async tests are reliable.

Table of Contents#

Understanding the "coroutine was never awaited" Warning#

Before diving into solutions, let’s clarify what the warning means.

What is a Coroutine?#

A coroutine is a special type of function defined with async def. Unlike regular functions, coroutines don’t execute immediately when called—they return a coroutine object that must be awaited (using await) to run. For example:

async def fetch_data():  
    # Simulate async work (e.g., API call)  
    await asyncio.sleep(1)  
    return {"data": "test"}  
 
# This returns a coroutine object, but does NOT execute the function  
coro = fetch_data()  

If you forget to await coro, Python throws RuntimeWarning: coroutine 'fetch_data' was never awaited because the coroutine is created but never run.

Why This Warning Matters in Tests#

In testing, unawaited coroutines mean your async code isn’t actually being executed. For example, if your test checks for an error in an async function but forgets to await it, the test will finish before the coroutine runs—passing incorrectly because the error is never raised.

Why Tests Pass When They Should Fail#

Let’s make this concrete with an example. Suppose you have an async function that intentionally raises an error, and you write a test to verify it fails:

# async_code.py  
import asyncio  
 
async def risky_operation():  
    await asyncio.sleep(0.1)  
    raise ValueError("Critical error!")  # This should cause the test to fail  
# test_async_code.py  
import pytest  
from async_code import risky_operation  
 
def test_risky_operation_fails():  
    # ❌ Mistake: Calling the async function without awaiting it  
    risky_operation()  # Returns a coroutine object, but does NOT run  
 
# Run with: pytest test_async_code.py -v  

What Happens?#

  • The test test_risky_operation_fails is a synchronous function.
  • risky_operation() is called but not awaited, so it returns a coroutine object and does nothing else.
  • The test finishes immediately, with no exceptions raised.
  • Pytest reports: PASSED (incorrectly!).

The test should fail because risky_operation raises ValueError, but it passes because the coroutine was never executed.

Common Causes of Unawaited Coroutines in Pytest#

To fix the warning and ensure tests behave correctly, first identify the root cause. Here are the most common mistakes:

1. Forgetting await in the Test#

The simplest error: calling an async function in a test but omitting await.

async def test_my_function():  
    # ❌ Unawaited coroutine  
    my_async_function()  # Should be: await my_async_function()  

2. Using Synchronous Test Functions with Async Code#

Pytest runs synchronous test functions (def test_*) in a synchronous context. If you call async code inside them without await, the coroutine is never executed.

3. Not Using pytest-asyncio#

Pytest doesn’t natively support async test functions. Without the pytest-asyncio plugin, async tests (defined with async def test_*) will fail silently or throw errors.

4. Incorrectly Structured Async Fixtures#

Async fixtures (e.g., for setup/teardown) must also be awaited or marked properly. For example:

# ❌ Unawaited async fixture  
@pytest.fixture  
def async_fixture():  
    return asyncio.sleep(1)  # Returns a coroutine, not awaited  

Step-by-Step Solutions to Fix the Warning#

Let’s resolve the issue with actionable steps, using the earlier risky_operation example.

Step 1: Install pytest-asyncio#

Pytest requires the pytest-asyncio plugin to run async tests. Install it first:

pip install pytest-asyncio  

Step 2: Mark Tests as Async and Use await#

Async tests must:

  • Be defined with async def.
  • Be marked with @pytest.mark.asyncio (to tell pytest to run them in an async context).
  • Explicitly await all async code.

Fixed Test:

# test_async_code.py  
import pytest  
from async_code import risky_operation  
 
@pytest.mark.asyncio  # ✅ Mark test as async  
async def test_risky_operation_fails():  # ✅ Test function is async  
    with pytest.raises(ValueError) as exc_info:  
        await risky_operation()  # ✅ Await the coroutine  
 
    assert "Critical error!" in str(exc_info.value)  

What Happens Now?#

  • @pytest.mark.asyncio tells pytest to run the test in an async event loop.
  • await risky_operation() executes the coroutine, triggering the ValueError.
  • pytest.raises(ValueError) catches the error, and the test fails correctly (as intended).

Step 3: Check for Unawaited Coroutines in Fixtures#

If you use async fixtures (e.g., to set up an async database connection), ensure they’re marked with async and properly awaited.

Bad Fixture (Unawaited):

@pytest.fixture  
def db_connection():  
    # ❌ Returns a coroutine, not awaited  
    return connect_to_db_async()  # Async function  

Fixed Fixture:

@pytest.fixture  
async def db_connection():  # ✅ Fixture is async  
    conn = await connect_to_db_async()  # ✅ Await the coroutine  
    yield conn  
    await conn.close()  # ✅ Cleanup (also async)  

Step 4: Avoid Mixing Sync and Async Code Unintentionally#

Never call async functions from synchronous test functions unless you explicitly run them in an event loop (not recommended—use async def tests instead).

Bad (Sync Test with Async Code):

def test_sync_with_async():  
    # ❌ Unawaited coroutine  
    async_function()  

Good (Async Test):

@pytest.mark.asyncio  
async def test_async_properly():  
    await async_function()  # ✅ Awaited  

Advanced Scenarios: Edge Cases and Workarounds#

Scenario 1: Using Alternate Async Libraries (Trio, Curio)#

If you use async libraries like trio or curio instead of asyncio, pytest-asyncio may need extra configuration. Install the library-specific plugin:

  • For Trio: pip install pytest-trio and use @pytest.mark.trio.
  • For Curio: pip install pytest-curio and use @pytest.mark.curio.

Scenario 2: Testing Async Code in Legacy Sync Tests#

If you must run async code from a synchronous test (e.g., in a legacy codebase), explicitly run the event loop with asyncio.run():

def test_legacy_sync_test():  
    # Only use this if you CANNOT convert to async def  
    result = asyncio.run(async_function())  # Runs the coroutine in a new loop  
    assert result == "expected"  

⚠️ Note: asyncio.run() creates a new event loop each time, which is slower than pytest-asyncio’s shared loop. Prefer async def tests when possible.

Scenario 3: Unawaited Coroutines in Callbacks#

If your code uses async callbacks (e.g., with add_done_callback), ensure they’re awaited. For example:

# ❌ Unawaited callback  
future = asyncio.Future()  
future.add_done_callback(lambda f: process_result(f.result()))  # Async?  
 
# ✅ Await the callback explicitly  
async def handle_future(future):  
    result = await future  
    await process_result(result)  # Assume process_result is async  
 
future.add_done_callback(lambda f: asyncio.create_task(handle_future(f)))  

Best Practices to Avoid the Warning#

  1. Always Use pytest-asyncio for async tests. It handles event loops and ensures async tests run correctly.
  2. Mark Async Tests Explicitly with @pytest.mark.asyncio and define them as async def.
  3. Await All Coroutines in tests and fixtures. If a function returns a coroutine, it must be awaited.
  4. Enable Warnings as Errors in pytest.ini to catch unawaited coroutines early:
    [pytest]  
    filterwarnings =  
        error::RuntimeWarning  
  5. Use Static Analysis (e.g., mypy or Pyright) to detect unawaited coroutines in your codebase:
    mypy --strict test_async_code.py  

Conclusion#

The RuntimeWarning: coroutine was never awaited is a critical red flag in async testing. It signals that your test isn’t executing async code, leading to false positives. By using pytest-asyncio, marking tests as async def, and ensuring all coroutines are awaited, you can fix the warning and ensure tests pass/fail as intended.

Follow the best practices outlined here to write reliable async tests and avoid silent failures in your codebase.

References#