En el tema anterior usamos fixtures dentro de un archivo de pruebas. En proyectos reales, muchas fixtures se comparten entre varios archivos y carpetas.
En este tema veremos cómo controlar el alcance de una fixture con scope y cómo compartir fixtures usando conftest.py, el archivo especial que pytest carga automáticamente.
conftest.py permite compartir fixtures sin importarlas manualmente en cada archivo de prueba.
Crea un proyecto nuevo:
mkdir pytest-conftest-demo
cd pytest-conftest-demo
Si pytest no está instalado en el entorno activo:
python -m pip install pytest
Crea estas carpetas:
mkdir app
mkdir tests
mkdir tests\unit
mkdir tests\integration
En Linux o macOS:
mkdir -p app tests/unit tests/integration
La estructura permitirá compartir fixtures entre pruebas unitarias e integración.
Dentro de app, crea usuarios.py:
def crear_usuario(nombre, edad):
return {
"nombre": nombre.strip().title(),
"edad": edad,
"activo": edad >= 18,
}
def obtener_activos(usuarios):
return [usuario for usuario in usuarios if usuario["activo"]]
Ahora crea app\pedidos.py:
def crear_pedido(usuario, items):
return {
"usuario": usuario,
"items": items,
"total": sum(item["precio"] * item["cantidad"] for item in items),
}
def pedido_es_valido(pedido):
return pedido["usuario"]["activo"] and pedido["total"] > 0
Dentro de la carpeta tests, crea conftest.py:
import pytest
from app.usuarios import crear_usuario
@pytest.fixture
def usuario_activo():
return crear_usuario("Ana", 25)
@pytest.fixture
def usuario_menor():
return crear_usuario("Luis", 15)
@pytest.fixture
def items_pedido():
return [
{"nombre": "Teclado", "precio": 50000, "cantidad": 1},
{"nombre": "Mouse", "precio": 12000, "cantidad": 2},
]
Las pruebas dentro de tests podrán usar estas fixtures sin importarlas.
Dentro de tests\unit, crea test_usuarios.py:
from app.usuarios import obtener_activos
def test_usuario_activo_tiene_estado_activo(usuario_activo):
assert usuario_activo["activo"] is True
def test_usuario_menor_no_tiene_estado_activo(usuario_menor):
assert usuario_menor["activo"] is False
def test_obtener_activos(usuario_activo, usuario_menor):
resultado = obtener_activos([usuario_activo, usuario_menor])
assert resultado == [usuario_activo]
Observa que no importamos usuario_activo ni usuario_menor. pytest las encuentra en conftest.py.
Dentro de tests\integration, crea test_pedidos.py:
from app.pedidos import crear_pedido, pedido_es_valido
def test_crear_pedido_calcula_total(usuario_activo, items_pedido):
pedido = crear_pedido(usuario_activo, items_pedido)
assert pedido["total"] == 74000
def test_pedido_de_usuario_activo_es_valido(usuario_activo, items_pedido):
pedido = crear_pedido(usuario_activo, items_pedido)
assert pedido_es_valido(pedido) is True
def test_pedido_de_usuario_menor_no_es_valido(usuario_menor, items_pedido):
pedido = crear_pedido(usuario_menor, items_pedido)
assert pedido_es_valido(pedido) is False
Las fixtures compartidas funcionan en subcarpetas de tests.
Desde la raíz del proyecto, ejecuta:
python -m pytest
La salida esperada será similar a:
collected 6 items
tests/integration/test_pedidos.py ... [ 50%]
tests/unit/test_usuarios.py ... [100%]
6 passed in 0.03s
El alcance define cada cuánto se crea una fixture. Por defecto, el alcance es function: se crea una vez por cada prueba que la usa.
@pytest.fixture(scope="function")
def usuario_activo():
return crear_usuario("Ana", 25)
Este comportamiento es el más seguro para datos mutables porque aísla una prueba de otra.
scope="function" crea una instancia nueva para cada prueba:
@pytest.fixture(scope="function")
def items_pedido():
return [
{"nombre": "Teclado", "precio": 50000, "cantidad": 1},
{"nombre": "Mouse", "precio": 12000, "cantidad": 2},
]
Si una prueba modifica la lista, otra prueba recibirá una lista nueva.
scope="module" crea la fixture una vez por archivo de prueba:
@pytest.fixture(scope="module")
def configuracion_impuestos():
return {
"iva": 21,
"moneda": "ARS",
}
Puede ser útil para datos de configuración que no se modifican durante las pruebas.
scope="session" crea la fixture una vez para toda la ejecución de pruebas:
@pytest.fixture(scope="session")
def configuracion_global():
return {
"entorno": "testing",
"version_api": "v1",
}
Este alcance conviene para datos costosos de crear y que no deben modificarse.
Agrega estas fixtures a tests\conftest.py:
@pytest.fixture(scope="module")
def configuracion_impuestos():
return {
"iva": 21,
"moneda": "ARS",
}
@pytest.fixture(scope="session")
def configuracion_global():
return {
"entorno": "testing",
"version_api": "v1",
}
Usaremos estas fixtures como datos de solo lectura.
Agrega estas pruebas en tests\unit\test_configuracion.py:
def test_configuracion_impuestos(configuracion_impuestos):
assert configuracion_impuestos["iva"] == 21
assert configuracion_impuestos["moneda"] == "ARS"
def test_configuracion_global(configuracion_global):
assert configuracion_global["entorno"] == "testing"
assert configuracion_global["version_api"] == "v1"
Las fixtures siguen disponibles sin importación manual.
Para ver fixtures propias y de pytest:
python -m pytest --fixtures
Para una salida más enfocada, puedes revisar el archivo conftest.py y los nombres usados como argumentos en las pruebas.
La ubicación de conftest.py define qué pruebas pueden usar sus fixtures. Un archivo en tests queda disponible para pruebas dentro de tests y sus subcarpetas.
tests/
|-- conftest.py
|-- unit/
| `-- test_usuarios.py
`-- integration/
`-- test_pedidos.py
Si colocas otro conftest.py dentro de una subcarpeta, sus fixtures quedan más localizadas.
pytest-conftest-demo/
|-- app/
| |-- pedidos.py
| `-- usuarios.py
`-- tests/
|-- conftest.py
|-- integration/
| `-- test_pedidos.py
`-- unit/
|-- test_configuracion.py
`-- test_usuarios.py
| Alcance | Cuándo se crea | Uso habitual |
|---|---|---|
function |
Una vez por prueba. | Datos mutables, objetos que cada prueba modifica. |
module |
Una vez por archivo de prueba. | Configuración de solo lectura para un módulo. |
session |
Una vez por ejecución completa. | Datos globales costosos de crear y no modificables. |
conftest.py: no hace falta; pytest las descubre automáticamente.session con objetos mutables: una prueba puede afectar a otra.conftest.py superior.function y amplía solo si hay una razón clara.mkdir pytest-conftest-demo
cd pytest-conftest-demo
python -m pip install pytest
mkdir app
mkdir tests
mkdir tests\unit
mkdir tests\integration
python -m pytest
python -m pytest -v
python -m pytest --fixtures
conftest.py permite compartir fixtures sin importarlas.tests\conftest.py están disponibles para pruebas dentro de tests.function.module y session sirven para datos reutilizables de solo lectura.function.En este tema vimos cómo reutilizar fixtures con conftest.py y cómo controlar su alcance con scope. Esta organización permite compartir preparación común sin duplicar código en cada archivo de prueba.
En el próximo tema trabajaremos con pruebas de clases, métodos y cambios de estado.