Módulo 5: Migración de Frameworks y Lenguajes
Migration Testing — Verificar Equivalencia
Migration Testing — Verificar Equivalencia
Descripción de la cápsula
El principio más importante de toda migración es: el código nuevo debe hacer exactamente lo mismo que el código viejo. No "más o menos lo mismo" — exactamente. La forma de verificar esto es con migration tests: tests que llaman al endpoint viejo y al nuevo con los mismos inputs y comparan los outputs.
En esta cápsula vas a aprender a escribir tres tipos de migration tests: tests de equivalencia (old==new), tests de contrato (la API responde con la estructura esperada), y tests de rollback (puedes volver atrás si algo falla). Claude Code puede generar todos estos tests analizando el código viejo y el nuevo.
Tests de Equivalencia
El principio
# Para cualquier input válido:
flask_response = flask_app.handle(request)
fastapi_response = fastapi_app.handle(request)
assert flask_response == fastapi_response # Deben ser iguales
Implementación con pytest
# tests/test_equivalence.py
import pytest
from flask_app import app as flask_app
from fastapi.testclient import TestClient
from fastapi_app import app as fastapi_app
flask_client = flask_app.test_client()
fastapi_client = TestClient(fastapi_app)
class TestEquivalence:
"""Verifica que Flask y FastAPI producen las mismas respuestas."""
@pytest.mark.parametrize("path", [
"/health",
"/users",
"/products",
])
def test_get_endpoints_equivalent(self, path):
flask_r = flask_client.get(path)
fastapi_r = fastapi_client.get(path)
assert flask_r.status_code == fastapi_r.status_code
assert flask_r.get_json() == fastapi_r.json()
def test_create_user_equivalent(self):
data = {"name": "Test User", "email": "test@example.com"}
flask_r = flask_client.post("/users", json=data)
fastapi_r = fastapi_client.post("/users", json=data)
assert flask_r.status_code == fastapi_r.status_code
# Comparar estructura (IDs pueden diferir)
flask_body = flask_r.get_json()
fastapi_body = fastapi_r.json()
assert flask_body["name"] == fastapi_body["name"]
assert flask_body["email"] == fastapi_body["email"]
@pytest.mark.parametrize("bad_data,expected_status", [
({"name": ""}, 400),
({"email": "bad"}, 400),
({}, 400),
])
def test_validation_errors_equivalent(self, bad_data, expected_status):
flask_r = flask_client.post("/users", json=bad_data)
fastapi_r = fastapi_client.post("/users", json=bad_data)
# Ambas deben rechazar con el mismo status code
assert flask_r.status_code // 100 == fastapi_r.status_code // 100
# Nota: Flask retorna 400, FastAPI puede retornar 422
# Comparamos la "clase" de error (4xx), no el código exacto
Qué comparar y qué ignorar
| Comparar | Ignorar |
|---|---|
| Status code (o clase 4xx) | Headers del framework |
| Body structure | IDs auto-generados |
| Field names y types | Timestamps exactos |
| Error messages (semántica) | Error format (detail vs error) |
| Business logic results | Framework metadata |
Tests de Contrato
Verificar que la nueva API cumple el contrato
class TestFastAPIContract:
"""Verifica que FastAPI cumple el contrato de la API."""
def test_create_user_returns_required_fields(self):
data = {"name": "Ana", "email": "ana@test.com"}
response = fastapi_client.post("/users", json=data)
body = response.json()
assert "id" in body
assert "name" in body
assert "email" in body
assert isinstance(body["id"], int)
assert isinstance(body["name"], str)
def test_create_user_returns_201(self):
data = {"name": "Ana", "email": "ana@test.com"}
response = fastapi_client.post("/users", json=data)
assert response.status_code == 201
def test_invalid_input_returns_4xx(self):
response = fastapi_client.post("/users", json={})
assert 400 <= response.status_code < 500
def test_not_found_returns_404(self):
response = fastapi_client.get("/users/999999")
assert response.status_code == 404
Tests de Rollback
Verificar que puedes volver atrás
class TestRollback:
"""Verifica que la app Flask sigue funcional (rollback viable)."""
def test_flask_still_serves_all_endpoints(self):
"""Flask sigue respondiendo correctamente."""
assert flask_client.get("/health").status_code == 200
assert flask_client.get("/users").status_code == 200
assert flask_client.post("/users", json=valid_data).status_code == 201
def test_flask_database_still_consistent(self):
"""La DB compartida es consistente desde Flask."""
flask_client.post("/users", json=valid_data)
response = flask_client.get("/users")
assert len(response.get_json()) > 0
Generando Migration Tests con Claude Code
# Prompt:
> "Analiza flask_app.py y fastapi_app.py. Genera tests
de migración completos:
1. Tests de equivalencia para cada endpoint
2. Tests de contrato para FastAPI
3. Tests de rollback para Flask
Usa pytest con parametrize donde aplique.
Incluye happy paths y error cases."
Conexión con Proyecto
En el Proyecto del Módulo (cápsula 06), los tests de equivalencia son parte del entregable. Cada endpoint migrado debe tener un test que verifica old==new.
Troubleshooting
Problema 1: Flask retorna 400, FastAPI retorna 422
Causa: Pydantic validation errors son 422 por defecto.
Solución: Compara la clase de error (4xx), no el código exacto:
assert flask_r.status_code // 100 == fastapi_r.status_code // 100
Problema 2: JSON structure difiere ligeramente
Causa: Flask wrappea en {"error": "..."}, FastAPI en {"detail": "..."}.
Solución: Compara el significado, no la key:
assert "error" in flask_r.get_json() or "detail" in flask_r.get_json()
Problema 3: Orden de campos JSON diferente
Causa: JSON no garantiza orden de keys.
Solución: Compara como dict (Python ignora orden) o usa json.dumps(sort_keys=True).
Ejercicios
Ejercicio 1: Escribir test de equivalencia parametrizado (Fácil)
Escribe un test parametrizado que verifica equivalencia de GET para 5 rutas diferentes.
Ver solución
@pytest.mark.parametrize("path", [
"/health",
"/users",
"/products",
"/orders",
"/categories",
])
def test_get_equivalence(self, path):
flask_r = flask_client.get(path)
fastapi_r = fastapi_client.get(path)
assert flask_r.status_code == fastapi_r.status_code
if flask_r.status_code == 200:
assert flask_r.get_json() == fastapi_r.json()
Ejercicio 2: Test de equivalencia para POST con edge cases (Medio)
Escribe tests de equivalencia para POST /orders que incluya: happy path, items vacíos, user inexistente, y cantidad negativa.
Ver solución
class TestCreateOrderEquivalence:
def test_happy_path(self):
data = {"user_id": 1, "items": [{"product_id": 1, "qty": 2}]}
flask_r = flask_client.post("/orders", json=data)
fastapi_r = fastapi_client.post("/orders", json=data)
assert flask_r.status_code == fastapi_r.status_code
def test_empty_items(self):
data = {"user_id": 1, "items": []}
flask_r = flask_client.post("/orders", json=data)
fastapi_r = fastapi_client.post("/orders", json=data)
assert flask_r.status_code // 100 == fastapi_r.status_code // 100
def test_invalid_user(self):
data = {"user_id": 99999, "items": [{"product_id": 1, "qty": 1}]}
flask_r = flask_client.post("/orders", json=data)
fastapi_r = fastapi_client.post("/orders", json=data)
assert flask_r.status_code == fastapi_r.status_code
def test_negative_quantity(self):
data = {"user_id": 1, "items": [{"product_id": 1, "qty": -1}]}
flask_r = flask_client.post("/orders", json=data)
fastapi_r = fastapi_client.post("/orders", json=data)
assert flask_r.status_code // 100 == fastapi_r.status_code // 100
Errores Comunes en Migration Testing
Error 1: Comparar respuestas byte-a-byte
Síntoma: El 80% de tus tests fallan porque Flask retorna {"id": 1, "name": "x"} y FastAPI retorna {"name": "x", "id": 1}. JSON no garantiza orden.
Por qué pasa: Comparas strings JSON literales en lugar de comparar como diccionarios. Python compara dicts ignorando orden, pero strings no.
Cómo corregir: Siempre parsea a dict antes de comparar:
assert flask_r.get_json() == fastapi_r.json() # ✅ compara como dict
# NO: assert flask_r.text == fastapi_r.text # ❌ falla por orden
Error 2: Asumir que mismos status codes = mismos errores
Síntoma: Tu test dice assert flask.status == fastapi.status, pasa con 200, pero los bodies son completamente distintos.
Por qué pasa: Status code igual no implica equivalencia. La equivalencia incluye status + body + side effects (DB writes, emails enviados, etc.).
Cómo corregir: Verifica las tres dimensiones:
- Status code (o clase 4xx/5xx)
- Body structure (con campos comparables)
- Side effects (¿se creó el registro? ¿se envió el email?)
Error 3: Tests de equivalencia sin cleanup entre runs
Síntoma: Los tests pasan la primera vez, fallan la segunda. POST /users crea un usuario en Flask, otro en FastAPI, los IDs colisionan o la DB se llena.
Por qué pasa: No hay rollback de la DB entre tests. Cada POST acumula state.
Cómo corregir: Usa transactional fixtures de pytest o reset la DB entre tests. Si usas la misma DB para Flask y FastAPI, asegura que el cleanup limpia ambos paths:
@pytest.fixture(autouse=True)
def reset_db():
yield
db.session.rollback()
db.drop_all()
db.create_all()
Error 4: Solo testear happy paths
Síntoma: Migraste, todos los tests verdes, en producción los errores 4xx tienen formatos distintos y rompen los clientes.
Por qué pasa: Los happy paths suelen ser fáciles de migrar. Los error cases son donde Flask y FastAPI difieren más (Flask retorna 400 con texto, FastAPI 422 con JSON estructurado).
Cómo corregir: Para cada endpoint, escribe al menos:
- 1 happy path
- 1 validation error (input inválido)
- 1 not found (recurso inexistente)
- 1 unauthorized (si aplica)
- 1 edge case del dominio
Los ejercicios de esta cápsula muestran el patrón.
Error 5: No correr los tests durante el cutover
Síntoma: Cutover de Flask, todo se siente bien, descubres en producción que un endpoint sutil dejó de funcionar.
Por qué pasa: Una vez "terminada" la migración, los tests de equivalencia se sienten redundantes. Pero son tu última línea de defensa.
Cómo corregir: Mantén los tests de equivalencia hasta el cutover. Córrelos como parte del CI hasta el día que eliminas Flask. El día del cutover los puedes archivar (no eliminar — son referencia histórica).
Resumen
- Tests de equivalencia verifican que old==new para el mismo input
- Tests de contrato verifican que la nueva API cumple la estructura esperada
- Tests de rollback verifican que la app vieja sigue funcional
- Comparar significado, no formato exacto (400 vs 422, "error" vs "detail")
- Claude Code genera todos estos tests analizando ambas apps
- Las 3 dimensiones de equivalencia: status + body + side effects
- Tests cubren error cases, no solo happy paths
Próxima cápsula: Proyecto — Migración Flask→FastAPI completa.
Recursos Adicionales
- pytest - Parametrize - Tests parametrizados para equivalencia
- FastAPI TestClient - Testing en FastAPI
- Flask Testing - Testing en Flask
- Contract Testing - Pact - Framework de contract testing
- API Compatibility Testing - OpenAPI para validar contratos
- Hypothesis - Property-Based Testing - Generar inputs automáticamente