Módulo 2: Unit Tests con Claude Code
Pytest Patterns: Arrange-Act-Assert
Pytest Patterns: Arrange-Act-Assert
Descripción de la cápsula
En la cápsula anterior aprendiste cómo pedirle a Claude Code que genere tests de calidad: prompts específicos, categorías de escenarios, naming descriptivo. Pero cuando Claude Code te devuelve 20 tests, ¿cómo evalúas si están bien estructurados? ¿Cómo identificas un test confuso que mezcla setup, ejecución y verificación en un solo bloque indescifrable?
Esta cápsula te enseña el patrón Arrange-Act-Assert (AAA) — la estructura estándar que hace que cada test sea legible, debugeable y mantenible. También cubre las herramientas de pytest que Claude Code usa constantemente: fixtures para evitar repetición, assertions para validar resultados, y la organización de archivos que escala a proyectos reales. No es un curso de pytest completo — es exactamente lo que necesitas para entender, evaluar y mejorar lo que Claude Code genera.
Al final, reconocerás la estructura AAA en cualquier test, sabrás usar fixtures y assertions correctamente, y podrás pedirle a Claude Code "usa arrange-act-assert" con confianza porque entiendes qué estás pidiendo.
El Patrón Arrange-Act-Assert (AAA)
¿Qué es AAA?
El patrón Arrange-Act-Assert divide cada test en tres fases claras:
- ARRANGE: Preparas el estado — datos de entrada, objetos, configuración. Todo lo que el sistema necesita antes de ejecutar la acción.
- ACT: Ejecutas exactamente una acción — la función, método o comportamiento que estás probando.
- ASSERT: Verificas el resultado esperado — comparas el output con lo que debería ser.
La regla clave: una sola acción por test. Si hay múltiples acciones (o múltiples asserts que verifican cosas distintas), el test pierde foco y cuando falla no sabes exactamente qué falló.
ARRANGE: Configurando el estado
En esta fase creas los objetos, datos y condiciones necesarias. Sin esta preparación, no puedes ejecutar la acción de forma aislada.
# calculator.py
def add(a: float, b: float) -> float:
return a + b
def discount_price(price: float, discount_percent: float) -> float:
"""Apply discount and return rounded price."""
if price < 0 or discount_percent < 0 or discount_percent > 100:
raise ValueError("Invalid price or discount")
return round(price * (1 - discount_percent / 100), 2)
# tests/test_calculator.py
import pytest
from calculator import add, discount_price
def test_add_positive_numbers():
# ARRANGE: datos de entrada
a = 2
b = 3
# ACT: ejecutar la acción
result = add(a, b)
# ASSERT: verificar resultado
assert result == 5
def test_discount_price_ten_percent():
# ARRANGE: precio y descuento
price = 100.0
discount_percent = 10.0
# ACT: aplicar descuento
result = discount_price(price, discount_percent)
# ASSERT: verificar precio final
assert result == 90.0
Los comentarios explícitos (# ARRANGE, # ACT, # ASSERT) son opcionales pero útiles cuando aprendes. En tests maduros, la estructura es evidente por el espaciado en blanco entre las tres secciones.
ACT: Una sola acción
La fase ACT debe contener exactamente una llamada a la función o método que estás probando. Si tienes varias acciones, probablemente estás probando varias cosas — y cuando el test falle, no sabrás cuál falló.
# ❌ MAL: múltiples acciones
def test_user_operations():
user = User("alice") # ¿Setup? ¿Acción?
user.add_role("admin") # Acción 1
user.add_role("editor") # Acción 2
assert user.roles == ["admin", "editor"] # ¿Qué acción falló?
# ✅ BIEN: una acción por test
def test_add_role_append_to_list():
# ARRANGE
user = User("alice")
# ACT: una sola acción
user.add_role("admin")
# ASSERT
assert "admin" in user.roles
def test_add_multiple_roles_maintains_order():
# ARRANGE
user = User("alice")
# ACT: una acción compuesta pero conceptualmente una (add_multiple_roles)
user.add_roles(["admin", "editor"])
# ASSERT
assert user.roles == ["admin", "editor"]
Si tienes lógica compleja, extrae acciones auxiliares a funciones helper o usa múltiples tests.
ASSERT: Verificando el resultado
Los asserts deben verificar una expectativa clara. Evita asserts que validen muchas cosas a la vez; si uno falla, los demás no se ejecutan y pierdes información.
# ❌ MAL: varios asserts independientes
def test_api_response():
response = fetch_user(1)
assert response.status_code == 200
assert response.json["name"] == "Alice"
assert response.json["email"] == "alice@example.com"
assert len(response.json["roles"]) == 2
# Si falla en name, no sabes si email y roles están bien
# ✅ BIEN: un assert por test (o asserts que verifican un solo concepto)
def test_fetch_user_returns_200():
response = fetch_user(1)
assert response.status_code == 200
def test_fetch_user_returns_correct_name():
response = fetch_user(1)
assert response.status_code == 200 # Precondición para el assert real
assert response.json["name"] == "Alice"
A veces necesitas un assert de precondición (ej. status_code == 200) antes del assert principal. Eso es aceptable — pero si tienes 5 asserts verificando cosas distintas, considera dividir en tests separados.
Por qué AAA importa para claridad y debugging
Cuando un test falla, el mensaje de pytest te dice dónde falló (línea del assert) pero no por qué. Con AAA:
- ✅ Si falla en ASSERT, sabes que ARRANGE y ACT están bien — el problema es la lógica o los datos esperados.
- ✅ Si fallas antes (exception en ACT), sabes que ARRANGE podría estar mal o la función tiene un bug.
- ✅ Al leer el test, cualquiera entiende en 3 segundos qué se prueba.
Sin AAA, un test de 50 líneas con setup, loops, y múltiples asserts es una pesadilla para debugear.
pytest Fixtures: DRY en tus tests
El problema: repetición en cada test
Sin fixtures, repites el mismo setup en cada test:
def test_validate_email_standard():
email = "user@example.com" # ARRANGE
result = validate_email(email)
assert result == True
def test_validate_email_subdomain():
email = "user@sub.domain.com" # ARRANGE similar
result = validate_email(email)
assert result == True
def test_format_user_data():
user = {"name": "Alice", "email": "alice@example.com"} # Objeto repetido
result = format_user(user)
assert "Alice" in result
Cuando el formato de los datos de prueba cambia (ej. agregas un campo), debes tocar 20 tests. Las fixtures resuelven esto.
@pytest.fixture: definir datos reutilizables
Una fixture es una función decorada con @pytest.fixture. pytest la ejecuta automáticamente y pasa su resultado como parámetro al test.
# tests/test_user_validation.py
import pytest
from user_validator import validate_email
@pytest.fixture
def valid_email():
return "user@example.com"
@pytest.fixture
def sample_user():
return {"name": "Alice", "email": "alice@example.com", "age": 30}
def test_valid_email_returns_true(valid_email):
# ARRANGE: ya viene de la fixture
# ACT
result = validate_email(valid_email)
# ASSERT
assert result == True
def test_format_includes_name(sample_user):
from formatter import format_user
result = format_user(sample_user)
assert "Alice" in result
El nombre del parámetro del test debe coincidir con el nombre de la fixture. pytest inyecta el valor automáticamente.
Fixtures que dependen de otras fixtures
Las fixtures pueden usar otras fixtures como parámetros:
@pytest.fixture
def base_price():
return 100.0
@pytest.fixture
def quantity():
return 2
@pytest.fixture
def order(base_price, quantity):
"""Order construido a partir de base_price y quantity."""
return {"base_price": base_price, "quantity": quantity, "discount": 0}
def test_order_subtotal(order):
from pricing import calculate_subtotal
result = calculate_subtotal(order)
assert result == 200.0
conftest.py: fixtures compartidas
Cuando varias carpetas o archivos necesitan las mismas fixtures, las defines en conftest.py. pytest las descubre automáticamente.
project/
├── src/
│ ├── calculator.py
│ └── validator.py
├── tests/
│ ├── conftest.py # Fixtures visibles para todos los tests
│ ├── test_calculator.py
│ └── test_validator.py
# tests/conftest.py
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "email": "alice@example.com"}
@pytest.fixture
def empty_list():
return []
Ahora test_calculator.py y test_validator.py pueden usar sample_user y empty_list sin importarlos — pytest los inyecta si el parámetro coincide.
Regla: Las fixtures en conftest.py están disponibles para todos los tests en ese directorio y subdirectorios. Para fixtures usadas solo en un archivo, defínelas en ese archivo.
pytest Assert Patterns
assert con mensajes descriptivos
Un assert sin mensaje deja que pytest muestre solo el valor que falló. Con un mensaje, explicas el contexto:
def test_discount_calculation():
result = discount_price(100, 10)
assert result == 90.0, f"Expected 90.0 after 10% discount on 100, got {result}"
En pytest puedes usar el formato:
assert result == expected, "Mensaje cuando falla"
pytest.raises: verificando excepciones
Cuando la acción debe lanzar una excepción, usas pytest.raises:
import pytest
from calculator import discount_price
def test_negative_price_raises_value_error():
with pytest.raises(ValueError):
discount_price(-10, 5)
def test_negative_price_raises_with_message():
with pytest.raises(ValueError, match="Invalid price"):
discount_price(-10, 5)
pytest.raises(ValueError): verifica que se lance esa excepción.match="Invalid price": verifica que el mensaje contenga ese texto (regex).
Para capturar la excepción y hacer asserts sobre ella:
def test_exception_has_correct_message():
with pytest.raises(ValueError) as exc_info:
discount_price(-10, 5)
assert "price" in str(exc_info.value).lower()
pytest.approx: comparar floats
Los floats tienen errores de precisión. == puede fallar en valores que conceptualmente son iguales:
# ❌ Frágil
def test_member_discount():
result = calculate_price(100, 1, is_member=True)
assert result["final_price"] == 95.0 # Puede fallar: 94.99999999999999
# ✅ Robusto
def test_member_discount():
result = calculate_price(100, 1, is_member=True)
assert result["final_price"] == pytest.approx(95.0)
pytest.approx(95.0) usa tolerancia por defecto para comparación de floats. Para tolerancia explícita:
assert value == pytest.approx(95.0, rel=1e-2) # 1% de tolerancia relativa
assert value == pytest.approx(95.0, abs=0.01) # ±0.01 absoluto
Asserts con colecciones (listas, diccionarios)
Para listas y dicts, el assert directo funciona si el orden y estructura son exactos:
def test_sort_returns_ordered_list():
result = sort([3, 1, 2])
assert result == [1, 2, 3]
def test_user_dict_has_required_keys():
user = get_user(1)
assert user == {"id": 1, "name": "Alice", "email": "alice@example.com"}
Para verificar solo parte de la estructura:
def test_user_has_name_and_email():
user = get_user(1)
assert "name" in user
assert "email" in user
assert user["name"] == "Alice"
def test_items_are_in_result():
result = get_tags()
assert "python" in result
assert "testing" in result
Organizando tests
Convención test_ para descubrimiento
pytest descubre tests buscando funciones que empiecen con test_ y clases que empiecen con Test. No hace falta configuración.
# ✅ Descubierto por pytest
def test_add_numbers():
assert add(2, 3) == 5
# ❌ No descubierto
def check_addition():
assert add(2, 3) == 5
Clases para agrupar tests
Usa clases Test* para agrupar tests relacionados:
class TestValidateEmail:
def test_valid_email_returns_true(self):
assert validate_email("user@example.com") == True
def test_empty_email_returns_false(self):
assert validate_email("") == False
def test_missing_at_returns_false(self):
assert validate_email("userexample.com") == False
class TestValidateEmailErrorHandling:
def test_none_raises_type_error(self):
with pytest.raises(TypeError):
validate_email(None)
def test_int_raises_type_error(self):
with pytest.raises(TypeError):
validate_email(123)
Las clases no son obligatorias, pero ayudan cuando tienes muchos tests. Cada método debe empezar con test_.
Estructura del directorio tests/
Estructura típica:
project/
├── src/
│ └── my_module.py
├── tests/
│ ├── conftest.py # Fixtures compartidas
│ ├── __init__.py # Opcional: para imports
│ ├── test_my_module.py # Tests del módulo principal
│ └── test_validators.py # Tests agrupados por dominio
├── pytest.ini # Opcional: configuración
└── pyproject.toml
Alternativa con estructura espejo:
project/
├── src/
│ ├── validators/
│ │ └── email.py
│ └── pricing/
│ └── calculator.py
├── tests/
│ ├── conftest.py
│ ├── validators/
│ │ └── test_email.py
│ └── pricing/
│ └── test_calculator.py
Ubicación de conftest.py
tests/conftest.py: fixtures para todos los tests del proyecto.tests/integration/conftest.py: fixtures solo para tests de integración (ej. cliente de DB).
pytest aplica la fixture más cercana. Una fixture en tests/integration/conftest.py sobreescribe una del mismo nombre en tests/conftest.py solo para tests en tests/integration/.
Claude Code y los patrones pytest
Evaluar la estructura de tests generados
Cuando Claude Code genera tests, puedes evaluarlos rápido con AAA:
- ✅ ¿Hay una fase ARRANGE clara?
- ✅ ¿ACT tiene una sola acción?
- ✅ ¿ASSERT verifica una expectativa específica?
- ✅ ¿Hay repetición que podría ser fixture?
Si el test es un bloque monolítico sin separación, pide: "Reestructura este test usando arrange-act-assert, con una sola acción en ACT."
Pedir explícitamente arrange-act-assert
En tus prompts:
Genera unit tests para [función].
Usa el patrón arrange-act-assert en cada test:
- ARRANGE: prepara datos y objetos necesarios
- ACT: ejecuta exactamente una llamada a la función
- ASSERT: verifica el resultado esperado
Un assert por test.
Claude Code suele generar tests más estructurados cuando lo pides explícitamente.
Leer output de Claude Code con conocimiento de patrones
Si conoces AAA, fixtures y assertions:
- Sabes qué partes del test son configuración vs. verificación.
- Reconoces cuándo una fixture eliminaría repetición.
- Detectas
assert x == ycon floats y sugierespytest.approx. - Sugieres
pytest.raisescuando se espera una excepción.
Comparación: Con AAA vs. Sin AAA
| Aspecto | Sin AAA | Con AAA |
|---|---|---|
| Legibilidad | Setup, acción y asserts mezclados | Tres bloques claros |
| Debugging | Difícil saber en qué fase falla | Fácil identificar ARRANGE/ACT/ASSERT |
| Mantenimiento | Cambios propagan confusión | Cambios localizados |
| Evaluación de AI | Difícil juzgar calidad | Estructura clara para revisar |
Conexión con Proyecto
En el proyecto Unit test suite generada (cápsula 06), generarás una suite completa de tests para un módulo de utilidades (data_utils.py). Los tests que pidas a Claude Code deben seguir el patrón AAA.
Criterios prácticos:
- Cada test tiene ARRANGE, ACT y ASSERT bien separados.
- Usas fixtures (en
conftest.pyo en el archivo) para datos compartidos. - Usas
pytest.raisespara validaciones que lanzan excepciones. - Usas
pytest.approxpara comparaciones con floats. - Los tests están en
tests/con nombres claros.
Cuando evalúes lo que genere Claude Code, revisa la estructura AAA antes de dar por buenos los tests.
Troubleshooting
Problema 1: "Fixture not found" o el test no recibe la fixture
Causa: El nombre del parámetro del test no coincide con el nombre de la fixture.
Solución: El parámetro debe tener exactamente el mismo nombre que la fixture:
@pytest.fixture
def sample_user():
return {"name": "Alice"}
def test_user(sample_user): # Nombre idéntico
assert sample_user["name"] == "Alice"
Problema 2: assert falla con floats (94.999999 vs 95.0)
Causa: Comparación exacta de floats (==) con errores de precisión.
Solución: Usa pytest.approx:
assert result == pytest.approx(95.0)
Problema 3: pytest.raises no captura la excepción
Causa: La excepción se lanza fuera del bloque with (ej. en ARRANGE) o se captura en otro lugar.
Solución: La llamada que debe fallar debe estar dentro del with:
# ❌ MAL
with pytest.raises(ValueError):
value = get_invalid_value() # Excepción aquí podría no ser la esperada
process(value) # O aquí
# ✅ BIEN
with pytest.raises(ValueError):
discount_price(-10, 5) # La llamada que debe fallar
Problema 4: Tests que pasan de forma aislada pero fallan juntos
Causa: Estado compartido entre tests (variables globales, archivos, DB).
Solución: Usa fixtures que crean estado fresco para cada test. Evita mutar objetos globales en los tests.
Problema 5: conftest.py no es descubierto
Causa: Ubicación incorrecta o nombre del archivo.
Solución: El archivo debe llamarse exactamente conftest.py y estar en el directorio de tests o en un subdirectorio. Ejecuta pytest desde la raíz del proyecto: pytest tests/.
Ejercicios
Ejercicio 1: Identificar AAA (Fácil)
Reescribe este test aplicando arrange-act-assert con comentarios explícitos:
def test_full_name():
first = "John"
last = "Smith"
assert full_name(first, last) == "John Smith"
Ver solución
def test_full_name():
# ARRANGE: preparar datos de entrada
first = "John"
last = "Smith"
# ACT: ejecutar la función
result = full_name(first, last)
# ASSERT: verificar el resultado
assert result == "John Smith"
El test ya tenía la estructura; los comentarios la hacen explícita. En la fase ACT se guarda el resultado en una variable antes del assert, lo que mejora la legibilidad cuando el assert es más complejo.
Ejercicio 2: Crear una fixture (Fácil)
Tienes varios tests que usan el mismo usuario {"name": "Alice", "email": "alice@example.com"}. Crea una fixture sample_user y refactoriza al menos dos tests para usarla.
Ver solución
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "email": "alice@example.com"}
def test_format_includes_name(sample_user):
result = format_user(sample_user)
assert "Alice" in result
def test_validate_user_email(sample_user):
result = validate_user(sample_user)
assert result["valid"] == True
Si necesitas variaciones (ej. usuario inválido), puedes tener fixtures separadas:
@pytest.fixture
def invalid_user():
return {"name": "", "email": "not-an-email"}
Ejercicio 3: pytest.raises y pytest.approx (Medio)
Escribe dos tests para esta función: uno que verifique que lanza ValueError con input negativo, y otro que verifique el cálculo con floats usando pytest.approx:
def safe_sqrt(x: float) -> float:
if x < 0:
raise ValueError("x must be non-negative")
return x ** 0.5
Ver solución
import pytest
from my_module import safe_sqrt
def test_negative_input_raises_value_error():
with pytest.raises(ValueError, match="non-negative"):
safe_sqrt(-1)
def test_sqrt_of_two_approximate():
result = safe_sqrt(2)
assert result == pytest.approx(1.41421356)
Alternativa para el mensaje de excepción con regex:
def test_negative_input_raises_value_error():
with pytest.raises(ValueError, match="must be non-negative"):
safe_sqrt(-1.5)
Ejercicio 4: Separar test con múltiples acciones (Medio)
Este test hace demasiadas cosas. Divídelo en varios tests siguiendo AAA, con una acción por test:
def test_shopping_cart():
cart = ShoppingCart()
cart.add_item("apple", 1.0)
cart.add_item("banana", 0.5)
assert cart.total() == 1.5
cart.remove_item("apple")
assert cart.total() == 0.5
Ver solución
def test_add_item_increases_total():
# ARRANGE
cart = ShoppingCart()
# ACT
cart.add_item("apple", 1.0)
# ASSERT
assert cart.total() == 1.0
def test_add_multiple_items_sums_total():
# ARRANGE
cart = ShoppingCart()
# ACT
cart.add_item("apple", 1.0)
cart.add_item("banana", 0.5)
# ASSERT
assert cart.total() == pytest.approx(1.5)
def test_remove_item_decreases_total():
# ARRANGE
cart = ShoppingCart()
cart.add_item("apple", 1.0)
cart.add_item("banana", 0.5)
# ACT
cart.remove_item("apple")
# ASSERT
assert cart.total() == pytest.approx(0.5)
add_item se invoca dos veces en el tercer test, pero la acción que se prueba es remove_item. El add es solo parte del ARRANGE. Si quieres aislar más, puedes usar una fixture para un carrito pre-poblado.
Ejercicio 5: conftest.py (Medio)
Crea tests/conftest.py con una fixture sample_product que retorne {"name": "Widget", "price": 9.99}. Luego escribe un test en tests/test_products.py que use esa fixture sin definirla en el mismo archivo.
Ver solución
# tests/conftest.py
import pytest
@pytest.fixture
def sample_product():
return {"name": "Widget", "price": 9.99}
# tests/test_products.py
def test_product_has_name_and_price(sample_product):
assert sample_product["name"] == "Widget"
assert sample_product["price"] == 9.99
Para probar que la fixture se inyecta desde conftest.py, ejecuta:
pytest tests/test_products.py -v
Si el test pasa, pytest está resolviendo la fixture desde conftest.py.
Ejercicio 6: Prompt para Claude Code (Medio)
Escribe un prompt para Claude Code que pida unit tests para una función parse_csv_line(line: str) -> list[str] con estas condiciones: use arrange-act-assert, un assert por test, pytest.raises cuando corresponda, y nombres de test descriptivos.
Ver solución
Genera unit tests para parse_csv_line(line: str) -> list[str].
La función parsea una línea CSV y retorna una lista de strings.
Maneja comillas y comas dentro de campos.
Requisitos:
- Usa el patrón arrange-act-assert en cada test
- Un assert por test
- Usa pytest.raises para inputs inválidos que deben lanzar excepción
- Nombres descriptivos: test_parse_csv_line_[condición]_[resultado]
Categorías a cubrir:
- Happy path: líneas simples y con comillas
- Edge cases: línea vacía, campos vacíos, espacios
- Error handling: tipos incorrectos, formato inválido
Con esto Claude Code tendrá suficiente contexto para generar tests bien estructurados.
Resumen
- ✅ AAA estructura cada test en ARRANGE (preparar), ACT (ejecutar una acción), ASSERT (verificar).
- ✅ Una sola acción en ACT y un assert centrado en un comportamiento facilita debugging.
- ✅ Las fixtures (
@pytest.fixture) evitan repetición y se inyectan por nombre de parámetro. - ✅
conftest.pydefine fixtures compartidas para todos los tests del directorio. - ✅ Usa
pytest.raisespara excepciones ypytest.approxpara comparaciones con floats. - ✅ Organiza tests en
tests/, con prefijotest_y clasesTest*para agrupar. - ✅ Pedir "arrange-act-assert" a Claude Code mejora la calidad y estructura de los tests generados.
- ✅ Conocer estos patrones te ayuda a revisar y refinar tests producidos por la AI.
Próxima cápsula: @pytest.mark.parametrize para cubrir múltiples escenarios con un solo test.
Recursos Adicionales
- pytest: How to use fixtures - Documentación oficial de fixtures
- pytest: How to use parametrize - Próxima cápsula, referencia útil
- Arrange-Act-Assert (AAA) Pattern - Explicación del patrón AAA
- pytest: Assertions - Assertions y mensajes de error
- Real Python: pytest fixtures - Tutorial de pytest y fixtures
- Test Organizing (pytest docs) - Organización de proyectos con pytest
Módulo 2, Cápsula 03 — Testing with Claude Code Guide Estructura clara: arrange, act, assert