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 expresionesassert- 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
assertnormal, 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_*.pyo*_test.py - Funciones nombradas
test_* - Clases nombradas
Test*con métodostest_*
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
- Pytest Advanced Techniques - Dominando fixtures, parametrización y plugins
- Locust Python Load Testing - Testing de performance con Python
- Pirámide de Automatización de Testing - Dónde encajan los unit tests en tu estrategia
- Robot Framework - Testing keyword-driven para Python
