El Límite de las Pruebas
Los tests son el círculo arquitectónico más externo — los tests frágiles acoplados a detalles de implementación hacen el sistema rígido; una API de Test desacopla la estructura de tests de la estructura de la aplicación.
Por qué importa
Las pruebas siguen la Regla de Dependencia: son el círculo más externo, dependiendo hacia adentro del sistema. Nada en el sistema depende de las pruebas. Esto significa que las pruebas son reemplazables — puedes escribir mejores pruebas sin cambiar el código de producción, y puedes cambiar el código de producción sin cambiar las pruebas (siempre que se preserve el comportamiento).
El problema de las pruebas frágiles ocurre cuando las pruebas están acopladas a detalles de implementación en lugar de comportamiento. Una prueba que instancia repositorios de base de datos concretos, llama a métodos privados o verifica cadenas SQL específicas se romperá cada vez que cambie la implementación interna — aunque el comportamiento observable del sistema permanezca idéntico. Estas pruebas ralentizan la refactorización y dan confianza falsa.
La solución es una API de Pruebas: un conjunto de stubs en memoria que implementan las mismas interfaces abstractas que usa el código de producción. El Caso de Uso se prueba a través de su entrada/salida pública (DTOs), no a través de sus internos. Cuando la implementación del repositorio cambia de SQLAlchemy a Tortoise ORM, las pruebas — que ejercitan el Caso de Uso a través del stub en memoria — permanecen en verde.
✗El problema
Tests coupled to concrete repositories and private internal columns — every refactor breaks the test suite even when behavior is unchanged.
Bad
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from orders.models import OrderModel # concrete SQLAlchemy model
from orders.service import OrderService
@pytest.fixture
def session():
engine = create_engine("sqlite:///:memory:")
Session = sessionmaker(bind=engine)
OrderModel.metadata.create_all(engine)
return Session()
def test_place_order_saves_to_db(session):
service = OrderService(session) # depends on real session
service.place_order("user-1", [{"sku": "A1", "qty": 1, "price": 10.0}])
# Checking internal DB state — implementation detail
row = session.query(OrderModel).filter_by(user_id="user-1").first()
assert row._status_code == 1 # private column name!
# Rename _status_code: test breaks.
# Switch from SQLAlchemy to Tortoise: all tests break.
# Tests are testing HOW, not WHAT.
import { DataSource } from "typeorm";
import { OrderEntity } from "./OrderEntity";
import { PlaceOrderUseCase } from "./PlaceOrderUseCase";
import { TypeOrmOrderRepo } from "./TypeOrmOrderRepo";
describe("PlaceOrder", () => {
it("saves order to DB", async () => {
const ds = new DataSource({ type: "sqlite", database: ":memory:",
entities: [OrderEntity], synchronize: true });
await ds.initialize();
const repo = new TypeOrmOrderRepo(ds); // concrete dependency
const useCase = new PlaceOrderUseCase(repo);
await useCase.execute({ userId: "u1", items: [{ sku: "A1", qty: 1, price: 10 }] });
// Checking private DB column:
const raw = await ds.getRepository(OrderEntity).findOneBy({ userId: "u1" });
expect(raw!._internalStatusCode).toBe(1); // breaks when column is renamed
});
});
✓La solución
An in-memory stub implements the same abstract interface. Tests exercise the Use Case through its public DTO input/output — behavior, not implementation. Refactoring internals leaves the tests green.
Good
from orders.use_cases import PlaceOrderUseCase
from orders.repositories import OrderRepository # abstract interface
from orders.dtos import PlaceOrderRequest
class InMemoryOrderRepository(OrderRepository):
def __init__(self):
self._store = {}
def save(self, order) -> str:
self._store[order.order_id] = order
return order.order_id
def find_by_id(self, order_id: str):
return self._store.get(order_id)
def test_place_order_returns_pending_order():
repo = InMemoryOrderRepository()
use_case = PlaceOrderUseCase(repo)
request = PlaceOrderRequest(user_id="user-1",
items=[{"sku": "A1", "qty": 1, "price": 10.0}])
response = use_case.execute(request)
assert response.order_id is not None
assert response.status == "pending"
# No DB, no Flask, no network — runs in milliseconds.
# Refactor: rename any internal field — test stays green.
# Replace SQLAlchemy with anything — test stays green.
import { PlaceOrderUseCase } from "./PlaceOrderUseCase";
import { OrderRepository } from "./OrderRepository"; // abstract interface
import { Order } from "./Order";
class InMemoryOrderRepository implements OrderRepository {
private store = new Map<string, Order>();
async save(order: Order): Promise<string> { this.store.set(order.orderId, order); return order.orderId; }
async findById(id: string): Promise<Order | null> { return this.store.get(id) ?? null; }
}
describe("PlaceOrderUseCase", () => {
it("returns a pending order id", async () => {
const repo = new InMemoryOrderRepository();
const useCase = new PlaceOrderUseCase(repo);
const response = await useCase.execute({
userId: "u1", items: [{ sku: "A1", qty: 1, price: 10 }]
});
expect(response.orderId).toBeTruthy();
expect(response.status).toBe("pending");
});
});
// No DB, no TypeORM, no network — < 1ms.
// Refactor internals freely — tests stay green as long as behavior is unchanged.
💡Conclusión clave
Las pruebas que se rompen cuando refactorizas sin cambiar el comportamiento están probando la implementación, no el comportamiento. Te frenan en lugar de protegerte. Usa stubs en memoria detrás de interfaces abstractas y ejercita el sistema a través de los DTOs públicos de los Casos de Uso. Las pruebas que sobreviven la refactorización son las que vale la pena escribir.
🔧 Algunos ejercicios pueden tener errores. Si algo parece incorrecto, usa el botón Feedback (abajo a la derecha) para reportarlo — nos ayuda a corregirlo rápido.
Pista: Los tests que se rompen cuando refactorizas (sin cambiar el comportamiento) están probando la implementación, no el comportamiento. Te ralentizan en lugar de protegerte.
✗ Tu versión