pytest-asyncio permite que pytest ejecute corutinas en un event loop controlado. No convierte automáticamente la suite en paralela: evita llamar asyncio.run() manualmente en cada caso.

Primera prueba asíncrona

python -m pip install pytest pytest-asyncio
import pytest


async def buscar_usuario(user_id: int) -> dict[str, object]:
    return {"id": user_id, "activo": True}


@pytest.mark.asyncio
async def test_buscar_usuario() -> None:
    usuario = await buscar_usuario(7)
    assert usuario == {"id": 7, "activo": True}

Configura el modo asyncio en pyproject.toml o pytest. strict exige gestión explícita; auto reconoce las funciones async. Fijar la opción evita cambios entre entornos.

Recursos y límites

Las fixtures asíncronas deben abrir y cerrar clientes, conexiones o servidores en un scope apropiado. Evita compartir el loop y estado mutable sin motivo. Prueba la política de timeout de la aplicación en lugar de envolver todo con un plazo arbitrario.

Combina casos con pytest parametrize y dependencias con fixtures de pytest. Para grupos de tareas, consulta asyncio.TaskGroup.

Errores comunes

Configurar el descubrimiento

Guarda el modo en configuración versionada para que local y CI coincidan:

[tool.pytest.ini_options]
asyncio_mode = "strict"

Strict conviene si hay varios plugins asíncronos porque pytest-asyncio administra solo pruebas y fixtures marcadas. Auto reduce decoración en proyectos dedicados a asyncio. Ninguno ejecuta casos en paralelo; la opción define descubrimiento y propiedad del loop.

Si pytest omite una función async o dice que recibió una corutina, comprueba plugin, marcador y ruta de configuración. pytest --trace-config ayuda cuando editor y CI descubren plugins distintos.

Crear fixtures con vida clara

En strict usa pytest_asyncio.fixture y limpia después de yield.

import pytest_asyncio
import httpx

@pytest_asyncio.fixture
async def cliente():
    async with httpx.AsyncClient(base_url="https://example.test") as valor:
        yield valor

Prefiere scope de función. Uno mayor solo mejora velocidad si el recurso puede compartirse y su loop es compatible. Conexiones largas pueden filtrar transacciones, tareas y estado. Si el setup es costoso, restaura el estado y documenta quién cierra.

Probar excepciones y cancelación

Una API async también se define por sus fallos. Usa pytest.raises alrededor del await:

@pytest.mark.asyncio
async def test_rechaza_usuario() -> None:
    with pytest.raises(LookupError, match="usuario"):
        await cargar_usuario(-1)

En código concurrente, prueba qué tareas se cancelan cuando falla una hermana y verifica limpieza. No afirmes el orden exacto del scheduler salvo que sea contrato. Usa asyncio.Event para sincronizar, no un sleep breve que espera tener suerte.

La cancelación no es éxito normal. Cancela la tarea en una prueba, espérala y comprueba locks liberados, streams cerrados y ausencia de estado parcial.

Hacer timeouts deterministas

Prueba el mecanismo real, como asyncio.timeout() o la opción del cliente. Un fake puede esperar un evento nunca señalado y activar el límite sin servicio externo. Da margen al CI, pero inyecta un plazo corto en la aplicación.

No compares milisegundos exactos. Comprueba excepción, error de dominio y limpieza. Para retries, inyecta función de espera o reloj para no esperar segundos reales. Se prueban decisiones, no el scheduler.

Detectar tareas abandonadas

Una prueba puede pasar dejando una tarea en background. Eso genera avisos y altera casos posteriores. Las funciones que crean tareas deben definir propiedad: esperarlas, devolver un handle o usar TaskGroup. La prueba debe señalizar cierre y esperar el final.

Los avisos de tareas pendientes son defectos. Ejecuta con warnings visibles y considera tratarlos como errores en CI. Cierra generadores, clientes, pools y servidores mediante fixtures incluso si falla una assertion.

Aislar I/O en la frontera correcta

Sustituye transporte HTTP, repositorio, reloj o adaptador de cola, no métodos internos del loop. Un fake pequeño con el protocolo async suele ser más claro que un mock profundo. Usa AsyncMock cuando importen las llamadas y configura una especificación.

Mantén pocas pruebas de integración con base de datos o servidor local para verificar adaptadores. Necesitan datos únicos y teardown. La suite unitaria no debe alcanzar internet silenciosamente.

Parametrizar comportamiento async

pytest.mark.parametrize funciona igual con pruebas async. Úsalo para límites, estados y errores, con ids descriptivos. Evita una matriz enorme: separa parsing, timeout, cancelación y persistencia para que cada fallo sea informativo.

Comprueba resultados y efectos visibles. Reserva assertions de llamadas para interacciones que sean parte del contrato, no para detalles internos de implementación.

Mantener la suite portátil

Lista de revisión

Antes de integrar, ejecuta el caso aislado varias veces y luego toda la suite. Confirma que ninguno depende del orden, que los warnings permanecen visibles y que cada fixture cierra recursos incluso cuando ocurre una excepción. Cuando la automatización lo permita, prueba las versiones mínima y máxima de Python soportadas.

Comprueba el contrato público: resultados, excepciones, cancelación y efectos persistidos. Una prueba que inspecciona demasiadas tareas internas se vuelve frágil ante refactorizaciones legítimas. Usa nombres que describan escenario y resultado, separa preparación, acción y comprobación, y conserva un motivo claro por caso.

Durante la revisión busca time.sleep, clientes sin cerrar, tareas sin propietario y acceso real a red. Verifica además que el modo asyncio esté en configuración versionada y que las versiones del plugin coincidan entre desarrollo y CI. No ocultes carreras con repetición automática.

En proyectos con señales o subprocess, añade casos específicos por plataforma cuando el comportamiento sea realmente diferente. No marques como incompatible todo el archivo si solo una capacidad varía. La condición debe explicar la limitación y mantener ejecutándose la mayor parte de la suite.

Mide la duración por caso para descubrir fixtures lentas y esperas reales. Un timeout global no reemplaza los límites de la aplicación, pero puede impedir que un defecto bloquee todo el job. Conserva suficiente información de diagnóstico: nombre del test, excepción original, tareas pendientes y warnings.

Si una prueba necesita congelar el tiempo, usa una abstracción de reloj compatible con corutinas en vez de modificar el loop global. Para colas y workers, entrega eventos controlados y confirma acknowledgement, retry y descarte por separado. Así la prueba sigue describiendo semántica del sistema.

Loops y plataformas revelan supuestos ocultos. No dependas de estado global, orden implícito o un puerto manual. Deja que el sistema operativo asigne puertos locales. Fija versiones compatibles y revisa migraciones antes de cambiar scopes predeterminados.

Ejecuta primero el archivo concreto y luego toda la suite para detectar fugas. Si falla solo en CI, examina warnings, versiones, raíz de configuración y tareas pendientes antes de añadir retries. Repetir una carrera solo oculta el defecto.

No uses time.sleep() dentro de una corutina porque bloquea el loop. Usa dobles de prueba o asyncio.sleep() cuando esperar forme parte del comportamiento. Las pruebas unitarias no deben depender de servicios externos reales.

La documentación oficial de pytest-asyncio, consultada el 22 de julio de 2026, cubre modos, marcadores, fixtures y scopes. Revisa la versión que corresponda a tu dependencia fijada.