pytest
asynchronous testing
gevent
Python testing
concurrency

How to run tests asynchronously using pytest and gevent or another asynchronous approach?

Master System Design with Codemia

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

Introduction

Asynchronous testing in Python can mean two different things: testing code that is itself asynchronous, or trying to run multiple tests concurrently. Those are not the same problem, and pytest treats them differently. If your application uses gevent, asyncio, or another cooperative concurrency model, the clean approach is usually to test that code in its native style rather than trying to force the entire test suite to execute in parallel.

Decide What "Asynchronous Testing" Means

Most confusion comes from mixing these goals:

  • write tests for asynchronous application code
  • run many tests concurrently to speed up the suite
  • simulate concurrent clients in one test

pytest is excellent at structuring tests, but it does not automatically make tests asynchronous or concurrent. You need the right library or plugin for the concurrency model your code uses.

Testing gevent-Based Code

If the application uses gevent, the main thing is to let greenlets run and then wait for them deterministically inside the test. You do not usually need a special pytest async syntax for that.

Here is a simple example:

python
1import gevent
2
3
4def fetch_value(delay, value):
5    gevent.sleep(delay)
6    return value
7
8
9def test_gevent_greenlets():
10    g1 = gevent.spawn(fetch_value, 0.01, "a")
11    g2 = gevent.spawn(fetch_value, 0.02, "b")
12
13    gevent.joinall([g1, g2])
14
15    assert g1.value == "a"
16    assert g2.value == "b"

This test is still a normal pytest test function. The asynchronous behavior is handled by gevent itself.

If your code depends on monkey patching, apply it very early in the process, ideally before importing networking libraries that should be patched.

python
from gevent import monkey
monkey.patch_all()

That should usually live in test bootstrap code, not inside random test bodies.

Simulating Concurrent Work in One Test

A frequent requirement is not "run tests concurrently" but "exercise concurrent behavior within a single test". That works well with gevent.spawn and explicit joins.

python
1import gevent
2
3counter = []
4
5
6def worker(name):
7    gevent.sleep(0.01)
8    counter.append(name)
9
10
11def test_two_workers_complete():
12    jobs = [gevent.spawn(worker, "x"), gevent.spawn(worker, "y")]
13    gevent.joinall(jobs)
14
15    assert sorted(counter) == ["x", "y"]

That gives you deterministic coverage of concurrency logic without trying to execute unrelated test files in parallel.

For asyncio, Use the Right Plugin

If the code under test is based on asyncio, gevent is not the natural tool. Use pytest-asyncio and mark async tests explicitly.

python
1import asyncio
2import pytest
3
4
5async def fetch_value():
6    await asyncio.sleep(0.01)
7    return 42
8
9
10@pytest.mark.asyncio
11async def test_asyncio_function():
12    result = await fetch_value()
13    assert result == 42

This is usually the cleanest approach for modern Python async code. Do not mix gevent and asyncio in the same article mentally as if they were the same runtime model. They solve similar problems with different mechanics.

Running Tests Concurrently Is a Separate Concern

If your real goal is to speed up the suite, look at pytest-xdist for parallel execution across processes. That is not asynchronous testing in the application-code sense.

bash
pytest -n auto

This runs tests in parallel workers, but it introduces its own constraints around shared state, database fixtures, ports, and temporary files.

Use this when the suite is embarrassingly parallel, not as a fix for async application logic.

Keep Tests Deterministic

Asynchronous systems become hard to test when timing is implicit. A few habits help:

  • wait explicitly for all scheduled work to finish
  • avoid arbitrary sleep calls when event or state checks are possible
  • isolate shared state between tests
  • use timeouts around greenlet joins when hangs are possible

For example:

python
1import gevent
2
3
4def test_with_timeout():
5    job = gevent.spawn(lambda: gevent.sleep(0.01))
6    job.join(timeout=1.0)
7    assert job.ready()

That prevents a broken test from hanging the suite indefinitely.

Common Pitfalls

A common mistake is trying to make pytest itself "asynchronous" when the real requirement is just to test gevent or asyncio code correctly.

Another mistake is confusing concurrent test execution with testing concurrent behavior inside one test. Those are different concerns.

People also often use arbitrary sleeps as synchronization. That creates flaky tests instead of deterministic ones.

Finally, avoid mixing runtime models casually. If the app uses asyncio, prefer pytest-asyncio. If it uses gevent, test around gevent primitives directly.

Summary

  • Asynchronous testing can mean testing async code or running tests in parallel, and those are different goals
  • For gevent, ordinary pytest tests plus greenlets and explicit joins are often enough
  • For asyncio, use pytest-asyncio and async def tests
  • For faster suites, use process-level parallelism such as pytest-xdist, not runtime async tools
  • Keep async tests deterministic with explicit waits and timeouts
  • Choose the testing approach that matches the concurrency model your application actually uses

Course illustration
Course illustration

All Rights Reserved.