TL;DR

  • Pytest es el framework estándar de testing en Python — sintaxis simple, características poderosas
  • Escribe tests como funciones: def test_something(): con expresiones assert
  • Fixtures manejan setup/teardown con decorador @pytest.fixture
  • Parametriza para ejecutar mismo test con diferentes datos: @pytest.mark.parametrize
  • Rico ecosistema de plugins: pytest-cov (cobertura), pytest-xdist (paralelo), pytest-mock

Ideal para: Desarrolladores Python, proyectos Django/Flask/FastAPI, testing de data science Omite si: No usas Python (usa Jest para JS, JUnit para Java) Tiempo de lectura: 15 minutos

Tus tests de unittest son verbosos. Cada test necesita una clase. Los assertions no muestran qué falló. Los fixtures requieren jerarquías de herencia.

Pytest arregla todo esto. Escribe funciones simples. Obtén output detallado de fallos. Comparte fixtures sin herencia. Ejecuta tests en paralelo.

Este tutorial enseña pytest desde cero — instalación, assertions, fixtures, parametrización y los patrones que hacen tests Python mantenibles.

¿Qué es Pytest?

Pytest es un framework de testing Python que hace fácil escribir y ejecutar tests. Es el framework más popular en el ecosistema Python.

Por qué pytest sobre unittest:

  • Sintaxis más simple — tests son funciones, no clases
  • Mejores assertions — usa assert normal, obtén diffs detallados
  • Fixtures poderosos — dependency injection sin herencia
  • Parametrización — ejecuta mismo test con diferentes inputs
  • Ecosistema de plugins — 1000+ plugins para cada necesidad
  • Auto-descubrimiento — encuentra tests automáticamente

Instalación

# Instalar pytest
pip install pytest

# Verificar instalación
pytest --version

Estructura del Proyecto

my-project/
├── src/
│   └── calculator.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py          # Fixtures compartidos
│   ├── test_calculator.py
│   └── test_utils.py
├── pytest.ini               # Configuración
└── requirements.txt

Pytest auto-descubre tests en:

  • Archivos nombrados test_*.py o *_test.py
  • Funciones nombradas test_*
  • Clases nombradas Test* con métodos test_*

Escribiendo Tu Primer Test

# src/calculator.py
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# tests/test_calculator.py
from src.calculator import add, divide
import pytest

def test_add_positive_numbers():
    assert add(2, 3) == 5

def test_add_negative_numbers():
    assert add(-1, -1) == -2

def test_add_zero():
    assert add(5, 0) == 5

def test_divide():
    assert divide(10, 2) == 5

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

Ejecutando Tests

# Ejecutar todos los tests
pytest

# Output detallado
pytest -v

# Ejecutar archivo específico
pytest tests/test_calculator.py

# Ejecutar test específico
pytest tests/test_calculator.py::test_add_positive_numbers

# Ejecutar tests que coincidan con patrón
pytest -k "add"

# Parar en primer fallo
pytest -x

# Mostrar print statements
pytest -s

# Ejecutar con cobertura
pytest --cov=src

Assertions

Pytest usa la declaración assert de Python con mensajes de error detallados.

Assertions Básicos

def test_assertions():
    # Igualdad
    assert 1 + 1 == 2
    assert "hello" == "hello"
    assert [1, 2, 3] == [1, 2, 3]
    assert {"a": 1} == {"a": 1}

    # Truthiness
    assert True
    assert not False
    assert "string"  # no-vacío es truthy
    assert [1, 2]    # lista no-vacía es truthy

    # Comparaciones
    assert 5 > 3
    assert 5 >= 5
    assert 3 < 5

    # Pertenencia
    assert 2 in [1, 2, 3]
    assert "hello" in "hello world"
    assert "key" in {"key": "value"}

    # Identidad
    assert None is None
    a = [1, 2]
    b = a
    assert a is b

def test_approximate_equality():
    # Comparación de punto flotante
    assert 0.1 + 0.2 == pytest.approx(0.3)
    assert [0.1 + 0.2, 0.2 + 0.4] == pytest.approx([0.3, 0.6])

Testing de Excepciones

import pytest

def test_raises_exception():
    with pytest.raises(ValueError):
        int("not a number")

def test_raises_with_message():
    with pytest.raises(ValueError, match="invalid literal"):
        int("not a number")

def test_exception_info():
    with pytest.raises(ValueError) as exc_info:
        int("abc")
    assert "invalid literal" in str(exc_info.value)

Fixtures

Fixtures proveen dependencias para tests y manejan setup/teardown.

Fixture Básico

import pytest

@pytest.fixture
def sample_user():
    return {"name": "John", "email": "john@example.com", "age": 30}

def test_user_has_name(sample_user):
    assert sample_user["name"] == "John"

def test_user_has_email(sample_user):
    assert "@" in sample_user["email"]

Setup y Teardown

@pytest.fixture
def database_connection():
    # Setup
    connection = create_connection()

    yield connection  # Test se ejecuta aquí

    # Teardown
    connection.close()

def test_query(database_connection):
    result = database_connection.query("SELECT 1")
    assert result == 1

Scope de Fixtures

@pytest.fixture(scope="function")  # Default: ejecuta para cada test
def function_fixture():
    return create_resource()

@pytest.fixture(scope="class")  # Una vez por clase de tests
def class_fixture():
    return create_resource()

@pytest.fixture(scope="module")  # Una vez por archivo de tests
def module_fixture():
    return create_resource()

@pytest.fixture(scope="session")  # Una vez por sesión de tests
def session_fixture():
    return create_resource()

Fixtures Usando Otros Fixtures

@pytest.fixture
def user():
    return {"username": "testuser", "password": "secret"}

@pytest.fixture
def authenticated_client(user):
    client = APIClient()
    client.login(user["username"], user["password"])
    return client

def test_protected_endpoint(authenticated_client):
    response = authenticated_client.get("/api/protected")
    assert response.status_code == 200

conftest.py — Fixtures Compartidos

# tests/conftest.py
import pytest

@pytest.fixture
def app():
    """Crea aplicación para testing."""
    from myapp import create_app
    app = create_app(testing=True)
    return app

@pytest.fixture
def client(app):
    """Crea cliente de test."""
    return app.test_client()

@pytest.fixture
def db(app):
    """Crea tablas de base de datos."""
    from myapp import db
    with app.app_context():
        db.create_all()
        yield db
        db.drop_all()

Fixtures en conftest.py están automáticamente disponibles para todos los tests en el directorio y subdirectorios.

Parametrización

Ejecuta el mismo test con diferentes inputs.

Parametrización Básica

import pytest

@pytest.mark.parametrize("input,expected", [
    (1, 2),
    (2, 4),
    (3, 6),
    (0, 0),
    (-1, -2),
])
def test_double(input, expected):
    assert input * 2 == expected

Múltiples Parámetros

@pytest.mark.parametrize("a,b,expected", [
    (1, 1, 2),
    (2, 3, 5),
    (10, -5, 5),
    (0, 0, 0),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

Parametrizar con IDs

@pytest.mark.parametrize("email,valid", [
    ("user@example.com", True),
    ("invalid", False),
    ("@missing.com", False),
    ("user@.com", False),
], ids=["valid_email", "no_at_sign", "no_local_part", "no_domain"])
def test_email_validation(email, valid):
    assert is_valid_email(email) == valid

Markers

Los markers categorizan y seleccionan tests.

Markers Incluidos

import pytest

@pytest.mark.skip(reason="Aún no implementado")
def test_future_feature():
    pass

@pytest.mark.skipif(sys.platform == "win32", reason="Solo Unix")
def test_unix_feature():
    pass

@pytest.mark.xfail(reason="Bug conocido #123")
def test_known_bug():
    assert False  # No fallará el suite

@pytest.mark.xfail(strict=True)
def test_should_fail():
    assert False  # Debe fallar, sino el test falla

Markers Personalizados

# pytest.ini
# [pytest]
# markers =
#     slow: marca tests como lentos
#     integration: tests de integración

@pytest.mark.slow
def test_slow_operation():
    import time
    time.sleep(5)
    assert True

@pytest.mark.integration
def test_database_integration():
    # Requiere base de datos
    pass
# Ejecutar solo tests slow
pytest -m slow

# Omitir tests slow
pytest -m "not slow"

# Ejecutar tests integration o slow
pytest -m "integration or slow"

Mocking

Usa pytest-mock o unittest.mock para mocking.

pip install pytest-mock
# Usando pytest-mock
def test_api_call(mocker):
    mock_get = mocker.patch("requests.get")
    mock_get.return_value.json.return_value = {"data": "mocked"}

    from myapp import fetch_data
    result = fetch_data()

    assert result == {"data": "mocked"}
    mock_get.assert_called_once()

# Usando unittest.mock
from unittest.mock import patch, MagicMock

def test_with_mock():
    with patch("myapp.external_service") as mock_service:
        mock_service.return_value = "mocked result"
        from myapp import process_data
        result = process_data()
        assert result == "mocked result"

Mejores Prácticas

1. Un Assertion Por Test (Idealmente)

# Menos ideal: múltiples assertions
def test_user_creation():
    user = create_user("John", "john@example.com")
    assert user.name == "John"
    assert user.email == "john@example.com"
    assert user.id is not None

# Mejor: tests enfocados
def test_user_has_correct_name():
    user = create_user("John", "john@example.com")
    assert user.name == "John"

def test_user_has_correct_email():
    user = create_user("John", "john@example.com")
    assert user.email == "john@example.com"

2. Nombres de Tests Descriptivos

# Malo
def test_1():
    pass

# Bueno
def test_user_cannot_login_with_invalid_password():
    pass

3. Patrón Arrange-Act-Assert

def test_discount_calculation():
    # Arrange
    cart = ShoppingCart()
    cart.add_item(Product("Book", 100))
    cart.add_item(Product("Pen", 10))

    # Act
    total = cart.calculate_total(discount=0.1)

    # Assert
    assert total == 99  # (100 + 10) * 0.9

Testing Pytest con Asistencia de IA

Las herramientas de IA pueden acelerar la escritura de tests cuando se usan apropiadamente.

Lo que la IA hace bien:

  • Generar casos de test desde firmas de función
  • Crear datos de test parametrizados
  • Escribir boilerplate de fixtures
  • Sugerir edge cases para testear

Lo que aún necesita humanos:

  • Decidir qué comportamiento importa
  • Verificar que tests testean lo correcto
  • Debuggear interacciones complejas de fixtures
  • Entender requisitos de lógica de negocio

FAQ

¿Para qué se usa pytest?

Pytest es el framework de testing más popular de Python. Ejecuta unit tests, integration tests, functional tests y end-to-end tests con mínimo código boilerplate. Pytest es usado por proyectos importantes como Django, Flask, FastAPI, Requests y la mayoría del ecosistema Python.

¿Pytest es mejor que unittest?

Para la mayoría de casos, sí. Pytest tiene sintaxis más simple (funciones planas en lugar de clases), mejores mensajes de assertion (muestra actual vs esperado), fixtures poderosos sin herencia y rico ecosistema de plugins. Unittest es parte de la librería estándar de Python, pero requiere más código para lograr los mismos resultados.

¿Cómo ejecuto pytest?

Ejecuta pytest en tu terminal desde la raíz del proyecto. Automáticamente descubre archivos de test (nombrados test_*.py) y funciones de test (nombradas test_*). Usa pytest -v para output detallado mostrando cada test. Usa pytest -k "pattern" para ejecutar tests que coincidan con un patrón.

¿Qué son los pytest fixtures?

Fixtures son funciones decoradas con @pytest.fixture que proveen dependencias para tests como conexiones a base de datos, datos de test, clientes API u objetos configurados. Los tests reciben fixtures como argumentos de función. Fixtures manejan setup antes de tests y teardown después (usando yield).

Recursos Oficiales

Ver También