Cómo usar fixtures en pytest: guía práctica para QA Engineers

Cuando empecé a escribir tests con pytest, llegó un momento en que me di cuenta de que estaba repitiendo el mismo código de configuración en cada test. Abrir el navegador, navegar a la página, hacer login… todo igual en cada función. Los fixtures de pytest son la solución a ese problema y una vez que los entiendes no puedes imaginar escribir tests sin ellos.

En este artículo te explico qué son, cómo funcionan y cómo usarlos en proyectos reales con Playwright.


Qué es un fixture en pytest

Un fixture es una función que prepara el estado o los recursos que un test necesita para ejecutarse. En lugar de repetir el mismo código de configuración en cada test, defines un fixture una vez y pytest lo inyecta automáticamente en los tests que lo necesitan.

El nombre viene del inglés fixture, que en el contexto de testing significa «elemento fijo» — algo que se prepara antes del test y está listo cuando el test empieza.


El problema que resuelven los fixtures

Imagina que tienes diez tests que todos necesitan que el usuario esté logueado antes de ejecutarse. Sin fixtures, repetirías el mismo código de login en los diez tests.

python

def test_ver_perfil(page):
    page.goto("https://miweb.com/login")
    page.fill("#email", "usuario@test.com")
    page.fill("#password", "password123")
    page.click("#btn-login")
    # Aquí empieza el test real...

def test_editar_perfil(page):
    page.goto("https://miweb.com/login")
    page.fill("#email", "usuario@test.com")
    page.fill("#password", "password123")
    page.click("#btn-login")
    # Aquí empieza el test real...

Con un fixture, defines el login una vez y todos los tests que lo necesitan lo reciben automáticamente.


Tu primer fixture

Crear un fixture es tan simple como decorar una función con @pytest.fixture:

python

import pytest
from playwright.sync_api import Page

@pytest.fixture
def usuario_logueado(page: Page):
    page.goto("https://miweb.com/login")
    page.fill("#email", "usuario@test.com")
    page.fill("#password", "password123")
    page.click("#btn-login")
    page.wait_for_url("**/dashboard")
    return page

Y para usarlo en un test, simplemente lo añades como parámetro:

python

def test_ver_perfil(usuario_logueado):
    usuario_logueado.click("#mi-perfil")
    assert usuario_logueado.locator("h1").text_content() == "Mi Perfil"

def test_editar_perfil(usuario_logueado):
    usuario_logueado.click("#editar-perfil")
    assert usuario_logueado.locator("#formulario-edicion").is_visible()

pytest detecta automáticamente que test_ver_perfil necesita el fixture usuario_logueado, lo ejecuta antes del test y le pasa el resultado.


El yield — setup y teardown en un fixture

Una de las características más potentes de los fixtures de pytest es que puedes definir tanto la preparación como la limpieza en la misma función usando yield.

Todo lo que va antes del yield es el setup — se ejecuta antes del test. Todo lo que va después del yield es el teardown — se ejecuta después del test, tanto si pasa como si falla.

python

@pytest.fixture
def pagina_limpia(page: Page):
    # Setup — antes del test
    page.goto("https://miweb.com")
    page.evaluate("localStorage.clear()")
    page.evaluate("sessionStorage.clear()")
    
    yield page  # Aquí se ejecuta el test
    
    # Teardown — después del test
    page.evaluate("localStorage.clear()")
    page.close()

Esto garantiza que cada test empieza con un estado limpio y deja el entorno limpio después de ejecutarse, independientemente de si el test pasa o falla.


El scope de los fixtures

Por defecto, pytest crea un nuevo fixture para cada test. Pero a veces tiene sentido reutilizar el mismo fixture en varios tests para ahorrar tiempo. Eso se controla con el parámetro scope.

scope=»function» (por defecto) El fixture se crea y destruye para cada test. Es el más seguro porque garantiza total aislamiento entre tests.

scope=»class» El fixture se comparte entre todos los tests de una misma clase. Útil cuando tienes tests relacionados que pueden compartir estado.

scope=»module» El fixture se crea una vez por archivo de tests y se comparte entre todos los tests de ese archivo.

scope=»session» El fixture se crea una vez para toda la sesión de tests. Útil para recursos costosos como abrir una conexión a base de datos o lanzar un servidor.

python

@pytest.fixture(scope="session")
def navegador():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        yield browser
        browser.close()

@pytest.fixture(scope="function")
def pagina(navegador):
    page = navegador.new_page()
    yield page
    page.close()

En este ejemplo el navegador se lanza una sola vez para toda la sesión (caro) pero cada test recibe su propia página (barato y aislado).


El conftest.py — el lugar de los fixtures compartidos

Cuando tienes fixtures que quieres compartir entre varios archivos de tests, el lugar correcto para definirlos es en un archivo llamado conftest.py.

pytest busca automáticamente este archivo en el directorio de tests y en todos los directorios padre. No necesitas importarlo — pytest lo carga solo.

proyecto/
├── conftest.py          ← fixtures globales
├── tests/
│   ├── conftest.py      ← fixtures específicos de tests
│   ├── test_login.py
│   └── test_carrito.py
└── pages/
    └── login_page.py

Un conftest.py típico para proyectos con Playwright:

python

import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session")
def browser():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        yield browser
        browser.close()

@pytest.fixture
def page(browser):
    context = browser.new_context()
    page = context.new_page()
    yield page
    context.close()

@pytest.fixture
def pagina_home(page):
    page.goto("https://miweb.com")
    yield page

autouse — fixtures que se aplican automáticamente

Si necesitas que un fixture se ejecute para todos los tests sin que tengas que declararlo en cada uno, puedes usar autouse=True:

python

@pytest.fixture(autouse=True)
def limpiar_cookies(page):
    yield
    page.context.clear_cookies()

Este fixture se ejecutará automáticamente después de cada test, limpiando las cookies sin que ningún test tenga que declararlo explícitamente.

Úsalo con cuidado — los fixtures con autouse pueden hacer el comportamiento de los tests menos obvio para alguien que los lee por primera vez.


Fixtures con parámetros

Puedes parametrizar un fixture para que ejecute los tests con distintos valores automáticamente:

python

@pytest.fixture(params=["chromium", "firefox", "webkit"])
def navegador_multi(request):
    with sync_playwright() as p:
        browser = getattr(p, request.param).launch()
        yield browser
        browser.close()

Con este fixture, cada test que lo use se ejecutará tres veces — una por cada navegador. Es una forma muy limpia de hacer cross-browser testing sin duplicar código.


Fixtures en proyectos reales con Playwright

En mis proyectos de GitHub uso fixtures para gestionar el ciclo de vida del navegador, preparar el estado de login y limpiar el entorno entre tests. Puedes ver ejemplos reales en el repositorio qa-suite-saucedemo.

Si quieres profundizar en cómo organizar bien un proyecto de Playwright puedes leer qué es el Page Object Model o cómo hacer pruebas de regresión con Playwright.

Y si necesitas implementar una suite de tests profesional en tu proyecto puedes ver mis servicios en fatimaqa.com.

Scroll al inicio