A lo largo de la evolución de Test-Driven Development han cristalizado dos corrientes arquitectónicas predominantes: la Escuela Clásica (Chicago / Detroit) y la Escuela de Londres (Mockist / Outside-In). La divergencia entre ambas no atañe a preferencias cosméticas o sintácticas; refleja respuestas antitéticas a qué define una unidad de aislamiento (Unit of Isolation) y cómo las pruebas deben conducir la descomposición del software.
Escuela de Chicago (Classicist / Detroit) concibe los componentes como cajas negras. Evalúa el comportamiento observable de un conjunto cohesionado de objetos colaborativos utilizando instancias reales o implementaciones en memoria de alto rendimiento (In-Memory Fakes). Los mocks se restringen rigurosamente a fronteras no deterministas fuera del proceso (bases de datos, peticiones HTTP, reloj del sistema, sistema de archivos).
Escuela de Londres (Mockist / Outside-In) nació como un mecanismo de diseño arquitectónico descendente (Outside-In). El ingeniero inicia el desarrollo en el perímetro externo (Controlador API o formulario UI) e identifica y formaliza iterativamente los contratos de interacción entre roles antes de que los componentes subyacentes existan físicamente en el repositorio.
La diferencia cardinal se evidencia en cómo se articula el grafo de llamadas y en qué punto se anclan las expectativas del framework de pruebas.
+-----------------------------------------------------------------------+
| ESCUELA DE CHICAGO (STATE VERIFICATION) |
+-----------------------------------------------------------------------+
| [Test] |
| │ |
| ▼ (Invocación de método público) |
| +-------------------+ +-------------------+ |
| | SUT (OrderService)| -----> | Collaborator | (Objeto real) |
| +-------------------+ | (OrderLine / Tax) | |
| │ +-------------------+ |
| ▼ |
| [Aserción de estado final: expect(order.total).toBe(120)] |
+-----------------------------------------------------------------------+
+-----------------------------------------------------------------------+
| ESCUELA DE LONDRES (INTERACTION VERIFICATION) |
+-----------------------------------------------------------------------+
| [Test] |
| │ |
| ▼ (Invocación de método público) |
| +-------------------+ +-------------------+ |
| | SUT (OrderService)| -----> | Mock / Spy | (Doble de prueba) |
| +-------------------+ | (PaymentGateway) | |
| +-------------------+ |
| [Aserción de protocolo: expect(mockGateway.charge).toBeCalled()] |
+-----------------------------------------------------------------------+| Parámetro arquitectónico | Escuela de Chicago (Classicist) | Escuela de Londres (Mockist) | | :--- | :--- | :--- | | Unidad de aislamiento | Unidad de comportamiento. El objeto se prueba de forma cohesionada junto con sus dependencias reales. | Unidad de código / Clase. El módulo se aísla tajantemente de cualquier colaborador adyacente. | | Método de verificación | State Verification: Valida retornos de funciones, mutaciones de estado y persistencia. | Interaction Verification: Valida el protocolo de mensajes (métodos invocados, argumentos y frecuencias). | | Dirección del diseño | Inside-Out: Desde algoritmos de dominio hacia capas coordinadoras superiores. | Outside-In: Desde la interfaz perimetral (API / UI) hacia los roles colaboradores internos. | | Acoplamiento al código | Bajo. Refactorizar la arquitectura interna preserva los tests siempre que el contrato público se mantenga. | Alto. Alterar la firma de dependencias o el orden de llamadas rompe las pruebas. | | Localización del fallo | Difusa. Un fallo en un helper anidado puede propagarse y hacer fallar múltiples pruebas del servicio. | Precisa. Falla exclusivamente la suite de la clase que alberga directamente el error. | | Estrategia de dobles | Restringida a fronteras externas de I/O (bases de datos, HTTP, reloj, archivos). | Generalizada en todos los colaboradores de dominio e interfaces secundarias. |
Analizaremos la implementación de un caso de uso de tramitación de pedidos: PlaceOrderUseCase.
export interface OrderItem {
readonly productId: string;
readonly quantity: number;
readonly unitPriceCents: number;
}
export interface Order {
readonly id: string;
readonly customerEmail: string;
readonly items: readonly OrderItem[];
readonly totalCents: number;
readonly status: 'CREATED' | 'FAILED';
}
export interface InventoryRepository {
reserveStock(productId: string, quantity: number): Promise<boolean>;
}
export interface OrderRepository {
save(order: Order): Promise<void>;
findById(orderId: string): Promise<Order | null>;
}
export interface NotificationPort {
sendOrderConfirmation(email: string, orderId: string): Promise<void>;
}El enfoque clásico construye implementaciones funcionales en memoria (In-Memory Fakes). La prueba verifica la mutación de estado: el stock se descuenta y la orden queda almacenada en el repositorio.
import { describe, it, expect, beforeEach } from 'vitest';
import { PlaceOrderUseCase } from './place-order-use-case';
import { Order, OrderRepository, InventoryRepository, NotificationPort } from './types';
// In-Memory Fake: Cumple el contrato auténtico sin latencias externas de I/O
class FakeInventoryRepository implements InventoryRepository {
private stock = new Map<string, number>();
setStock(productId: string, qty: number): void {
this.stock.set(productId, qty);
}
getStock(productId: string): number {
return this.stock.get(productId) ?? 0;
}
async reserveStock(productId: string, quantity: number): Promise<boolean> {
const current = this.stock.get(productId) ?? 0;
if (current < quantity) return false;
this.stock.set(productId, current - quantity);
return true;
}
}
class FakeOrderRepository implements OrderRepository {
private orders = new Map<string, Order>();
async save(order: Order): Promise<void> {
this.orders.set(order.id, order);
}
async findById(orderId: string): Promise<Order | null> {
return this.orders.get(orderId) ?? null;
}
}
class FakeNotificationPort implements NotificationPort {
public sentEmails: Array<{ email: string; orderId: string }> = [];
async sendOrderConfirmation(email: string, orderId: string): Promise<void> {
this.sentEmails.push({ email, orderId });
}
}
describe('PlaceOrderUseCase (Escuela de Chicago)', () => {
let inventoryRepo: FakeInventoryRepository;
let orderRepo: FakeOrderRepository;
let notificationPort: FakeNotificationPort;
let useCase: PlaceOrderUseCase;
beforeEach(() => {
inventoryRepo = new FakeInventoryRepository();
orderRepo = new FakeOrderRepository();
notificationPort = new FakeNotificationPort();
useCase = new PlaceOrderUseCase(inventoryRepo, orderRepo, notificationPort);
});
it('tramita el pedido y descuenta el stock correctamente (State Verification)', async () => {
// Arrange: Cargar estado real en memoria
inventoryRepo.setStock('prod_laptop', 10);
// Act: Ejecutar el caso de uso público
const result = await useCase.execute({
orderId: 'ord_999',
customerEmail: '[email protected]',
items: [{ productId: 'prod_laptop', quantity: 2, unitPriceCents: 150000 }],
});
// Assert: Comprobación de mutación de estado
expect(result.success).toBe(true);
// 1. Verificamos el saldo reducido en el almacén
expect(inventoryRepo.getStock('prod_laptop')).toBe(8);
// 2. Verificamos la persistencia de la entidad
const savedOrder = await orderRepo.findById('ord_999');
expect(savedOrder).not.toBeNull();
expect(savedOrder?.totalCents).toBe(300000);
expect(savedOrder?.status).toBe('CREATED');
// 3. Verificamos el efecto colateral de notificación
expect(notificationPort.sentEmails).toHaveLength(1);
expect(notificationPort.sentEmails[0]?.orderId).toBe('ord_999');
});
});El enfoque de Londres aísla la clase PlaceOrderUseCase de implementaciones externas. Los colaboradores se sustituyen por espías que validan el protocolo de llamadas.
import { describe, it, expect, vi } from 'vitest';
import { PlaceOrderUseCase } from './place-order-use-case';
import { InventoryRepository, OrderRepository, NotificationPort } from './types';
describe('PlaceOrderUseCase (Escuela de Londres)', () => {
it('orquesta los puertos salientes conforme al protocolo definido (Interaction Verification)', async () => {
// Arrange: Creación de mocks estrictos para todos los colaboradores
const mockInventoryRepo: InventoryRepository = {
reserveStock: vi.fn().mockResolvedValue(true),
};
const mockOrderRepo: OrderRepository = {
save: vi.fn().mockResolvedValue(undefined),
findById: vi.fn().mockResolvedValue(null),
};
const mockNotificationPort: NotificationPort = {
sendOrderConfirmation: vi.fn().mockResolvedValue(undefined),
};
const useCase = new PlaceOrderUseCase(
mockInventoryRepo,
mockOrderRepo,
mockNotificationPort
);
// Act
const result = await useCase.execute({
orderId: 'ord_999',
customerEmail: '[email protected]',
items: [{ productId: 'prod_laptop', quantity: 2, unitPriceCents: 150000 }],
});
// Assert: Verificación rigurosa del protocolo de interacción
expect(result.success).toBe(true);
// 1. Verificamos que la reserva se ejecutó con los argumentos precisos
expect(mockInventoryRepo.reserveStock).toHaveBeenCalledTimes(1);
expect(mockInventoryRepo.reserveStock).toHaveBeenCalledWith('prod_laptop', 2);
// 2. Verificamos la orden de persistencia
expect(mockOrderRepo.save).toHaveBeenCalledTimes(1);
expect(mockOrderRepo.save).toHaveBeenCalledWith(
expect.objectContaining({
id: 'ord_999',
totalCents: 300000,
status: 'CREATED',
})
);
// 3. Verificamos la emisión del correo de confirmación
expect(mockNotificationPort.sendOrderConfirmation).toHaveBeenCalledTimes(1);
expect(mockNotificationPort.sendOrderConfirmation).toHaveBeenCalledWith(
'[email protected]',
'ord_999'
);
});
});La siguiente implementación desacoplada satisface plenamente las expectativas de ambas escuelas:
import {
InventoryRepository,
OrderRepository,
NotificationPort,
OrderItem,
Order,
} from './types';
export interface PlaceOrderCommand {
readonly orderId: string;
readonly customerEmail: string;
readonly items: readonly OrderItem[];
}
export interface PlaceOrderResult {
readonly success: boolean;
readonly error?: string;
}
export class PlaceOrderUseCase {
constructor(
private readonly inventoryRepo: InventoryRepository,
private readonly orderRepo: OrderRepository,
private readonly notificationPort: NotificationPort
) {}
public async execute(command: PlaceOrderCommand): Promise<PlaceOrderResult> {
// 1. Validar y reservar existencias
for (const item of command.items) {
const reserved = await this.inventoryRepo.reserveStock(
item.productId,
item.quantity
);
if (!reserved) {
return { success: false, error: `Insufficient stock for ${item.productId}` };
}
}
// 2. Calcular importe en centavos
const totalCents = command.items.reduce(
(acc, item) => acc + item.quantity * item.unitPriceCents,
0
);
const order: Order = {
id: command.orderId,
customerEmail: command.customerEmail,
items: command.items,
totalCents,
status: 'CREATED',
};
// 3. Persistir entidad
await this.orderRepo.save(order);
// 4. Emitir notificación
await this.notificationPort.sendOrderConfirmation(
command.customerEmail,
command.orderId
);
return { success: true };
}
}Intentar simular tipos inmutables como Money, CurrencyCode o DateRange. Los Value Objects carecen de identidad, no tienen efectos colaterales y son completamente deterministas. Mockearlos introduce fragilidad severa en la suite. Utilice instancias auténticas de Value Objects en ambas escuelas.
Validar el orden exacto o el número de llamadas de métodos de lectura intermedios que no tienen incidencia en el contrato final del negocio. Si un ingeniero optimiza el código agrupando lecturas en un lote (batch), el test mockista fallará pese a que el contrato se mantiene intacto. Mockee únicamente las mutaciones salientes (Command boundaries).
En la Escuela de Londres, la firma de una dependencia puede cambiar en producción, pero si los tests siguen utilizando un mockResolvedValue() desfasado, la suite permanecerá falsamente en verde. Mitigue este problema tipando estrictamente los mocks en TypeScript y complementando con pruebas de integración.
Emplear coerciones de tipos (vi.spyOn(sut as any, 'privateHelper')) para auditar funciones internas. Es un antipatrón perjudicial: acopla los tests a la estructura privada e imposibilita la refactorización. Compruebe el comportamiento exclusivamente mediante métodos públicos.
Entidades, reglas de negocio, agregados y cálculos. Aplicar Escuela de Chicago: objetos reales, verificación de estado y cero mocks.
Orquestación de flujos y coordinación. Aplicar Escuela de Londres: mockear puertos salientes para validar el protocolo.
Repositorios SQL, clientes HTTP, colas de mensajería. Aplicar pruebas de integración de Chicago con Testcontainers o bases en memoria.
Vistas y jerarquías de componentes. Aplicar filosofía de Chicago (React Testing Library): verificar roles ARIA en un árbol DOM real.