18. Alcance de fixtures y reutilización con conftest.py

18.1 Objetivo del tema

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.

Idea clave: conftest.py permite compartir fixtures sin importarlas manualmente en cada archivo de prueba.

18.2 Crear una carpeta de práctica

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

18.3 Crear la estructura del proyecto

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.

18.4 Crear el código principal

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

18.5 Crear un conftest.py

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.

18.6 Usar fixtures compartidas

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.

18.7 Usar las mismas fixtures en otro archivo

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.

18.8 Ejecutar las pruebas

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

18.9 Qué es el alcance de una fixture

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.

18.10 scope function

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.

18.11 scope module

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.

18.12 scope session

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.

18.13 Agregar fixtures con distintos alcances

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.

18.14 Usar fixtures de configuración

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.

18.15 Ver fixtures disponibles

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.

18.16 Dónde colocar conftest.py

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.

18.17 Estructura final del proyecto

pytest-conftest-demo/
|-- app/
|   |-- pedidos.py
|   `-- usuarios.py
`-- tests/
    |-- conftest.py
    |-- integration/
    |   `-- test_pedidos.py
    `-- unit/
        |-- test_configuracion.py
        `-- test_usuarios.py

18.18 Tabla de alcances

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.

18.19 Errores frecuentes

  • Importar fixtures desde conftest.py: no hace falta; pytest las descubre automáticamente.
  • Usar session con objetos mutables: una prueba puede afectar a otra.
  • Poner demasiadas fixtures en conftest.py: puede volverse difícil de navegar.
  • Crear nombres duplicados: una fixture local puede ocultar otra de un conftest.py superior.
  • Usar scope amplio sin necesidad: empieza con function y amplía solo si hay una razón clara.

18.20 Comandos usados en este tema

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

18.21 Qué debes recordar de este tema

  • conftest.py permite compartir fixtures sin importarlas.
  • Las fixtures de tests\conftest.py están disponibles para pruebas dentro de tests.
  • El alcance por defecto es function.
  • module y session sirven para datos reutilizables de solo lectura.
  • Los objetos mutables suelen ser más seguros con alcance function.
  • Un buen nombre de fixture hace más legible cada prueba.

18.22 Conclusión

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.