1200 lines
35 KiB
Markdown
1200 lines
35 KiB
Markdown
# Estrategia de pruebas (Testing Strategy)
|
|
|
|
Las pruebas de software permiten verificar comportamientos, detectar regresiones y proporcionar confianza durante el desarrollo y evolución de un sistema.
|
|
|
|
Su objetivo no es maximizar el número de pruebas ni alcanzar métricas de cobertura arbitrarias, sino comprobar que el software satisface correctamente sus requisitos.
|
|
|
|
Este documento desarrolla las prácticas de pruebas establecidas en el `SKILL.md` principal y complementa las recomendaciones de [tdd.md](tdd.md).
|
|
|
|
**Selecciona las pruebas según el comportamiento, el riesgo y el nivel de confianza necesario. No impongas una distribución fija de tipos de pruebas a todos los proyectos.**
|
|
|
|
## 1. La pirámide de pruebas (Testing Pyramid)
|
|
|
|
La pirámide de pruebas es un modelo que propone combinar diferentes niveles de verificación.
|
|
|
|
Tradicionalmente se representa de la siguiente forma:
|
|
|
|
```text id="gjbpx1"
|
|
/\
|
|
/ \
|
|
/ E2E\
|
|
/------\
|
|
/ \
|
|
/Integración\
|
|
/------------\
|
|
/ \
|
|
/ Unitarias \
|
|
/__________________\
|
|
```
|
|
|
|
La representación sugiere mantener una base amplia de pruebas rápidas y un conjunto más reducido de pruebas que requieren integrar numerosos componentes.
|
|
|
|
### Interpretación correcta
|
|
|
|
- Las pruebas unitarias suelen proporcionar retroalimentación rápida.
|
|
- Las pruebas de integración verifican colaboraciones y límites técnicos.
|
|
- Las pruebas E2E comprueban recorridos completos desde la perspectiva del usuario o consumidor.
|
|
|
|
Sin embargo, la pirámide no establece cantidades obligatorias.
|
|
|
|
La distribución adecuada depende de:
|
|
|
|
- Arquitectura del sistema.
|
|
- Complejidad del dominio.
|
|
- Integraciones externas.
|
|
- Riesgo funcional.
|
|
- Criticidad de los procesos.
|
|
- Facilidad para ejecutar los distintos tipos de pruebas.
|
|
- Coste de mantenimiento.
|
|
|
|
### Criterios de aplicación
|
|
|
|
- Utiliza pruebas unitarias cuando permitan comprobar comportamientos de forma rápida y aislada.
|
|
- Incorpora pruebas de integración para los contratos y límites relevantes.
|
|
- Utiliza pruebas E2E cuando sea necesario comprobar un flujo completo.
|
|
- Evita duplicar indiscriminadamente los mismos escenarios en todos los niveles.
|
|
- No presupongas que una prueba de mayor nivel proporciona siempre más confianza.
|
|
- Selecciona el nivel más adecuado para detectar cada tipo de regresión.
|
|
|
|
**Una estrategia de pruebas eficaz optimiza la confianza obtenida respecto al coste de ejecución y mantenimiento.**
|
|
|
|
---
|
|
|
|
## 2. Tipos de pruebas
|
|
|
|
### 2.1. Pruebas unitarias (Unit Tests)
|
|
|
|
Las pruebas unitarias verifican unidades de comportamiento en condiciones controladas.
|
|
|
|
Una unidad puede ser:
|
|
|
|
- Una función.
|
|
- Una clase.
|
|
- Un objeto de dominio.
|
|
- Un módulo pequeño.
|
|
- Un conjunto cohesionado de componentes.
|
|
|
|
No es obligatorio que una prueba unitaria compruebe exactamente una clase o función.
|
|
|
|
### Características habituales
|
|
|
|
- Ejecución rápida.
|
|
- Resultados deterministas.
|
|
- Preparación sencilla.
|
|
- Independencia de infraestructura externa cuando sea apropiado.
|
|
- Verificación de comportamientos concretos.
|
|
|
|
### Ejemplo
|
|
|
|
```typescript id="qv1r4u"
|
|
describe('Order', () => {
|
|
it('calculates the total of two items', () => {
|
|
const order = new Order();
|
|
|
|
order.addItem({ price: 100 });
|
|
order.addItem({ price: 50 });
|
|
|
|
expect(order.calculateTotal()).toBe(150);
|
|
});
|
|
});
|
|
```
|
|
|
|
El ejemplo utiliza importes numéricos para ilustrar la estructura.
|
|
|
|
En sistemas con reglas monetarias relevantes, utiliza una representación adecuada como `Money`.
|
|
|
|
### Cuándo utilizarlas
|
|
|
|
Son especialmente útiles para:
|
|
|
|
- Reglas de negocio.
|
|
- Cálculos.
|
|
- Validaciones.
|
|
- Objetos de valor.
|
|
- Entidades.
|
|
- Transformaciones.
|
|
- Algoritmos.
|
|
- Transiciones de estado.
|
|
|
|
### Criterios de aplicación
|
|
|
|
- Comprueba comportamientos observables.
|
|
- Evita dependencias externas innecesarias.
|
|
- Utiliza objetos reales cuando sean sencillos y deterministas.
|
|
- No introduzcas mocks para todas las dependencias por obligación.
|
|
- Evita pruebas que dependan de métodos privados o detalles internos.
|
|
- No escribas pruebas triviales que únicamente reproduzcan una implementación sin aportar confianza.
|
|
|
|
**Una prueba unitaria debe proporcionar confianza sobre un comportamiento, no verificar cómo está organizado internamente el código.**
|
|
|
|
---
|
|
|
|
### 2.2. Pruebas de integración (Integration Tests)
|
|
|
|
Las pruebas de integración verifican la colaboración entre componentes o sistemas.
|
|
|
|
Pueden incluir:
|
|
|
|
- Repositorios y bases de datos.
|
|
- APIs y servicios.
|
|
- Serialización y deserialización.
|
|
- Adaptadores.
|
|
- Sistemas de archivos.
|
|
- Colas de mensajería.
|
|
- Mecanismos de autenticación.
|
|
- Transacciones.
|
|
- Contratos entre módulos.
|
|
|
|
### Características habituales
|
|
|
|
- Mayor preparación que las pruebas unitarias.
|
|
- Dependencias reales o representativas.
|
|
- Verificación de límites entre componentes.
|
|
- Coste de ejecución variable.
|
|
- Detección de errores que no aparecen en pruebas completamente aisladas.
|
|
|
|
### Ejemplo con persistencia
|
|
|
|
```typescript id="170oc9"
|
|
describe('OrderRepository integration', () => {
|
|
let database: TestDatabase;
|
|
let repository: OrderRepository;
|
|
|
|
beforeAll(async () => {
|
|
database = await createTestDatabase();
|
|
repository = new PostgresOrderRepository(database.connection);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await database.close();
|
|
});
|
|
|
|
it('persists and retrieves an order', async () => {
|
|
const order = createTestOrder();
|
|
const expectedStatus = order.status;
|
|
const expectedTotal = order.calculateTotal();
|
|
|
|
await repository.save(order);
|
|
|
|
const retrieved = await repository.findById(order.id);
|
|
|
|
expect(retrieved).not.toBeNull();
|
|
expect(retrieved?.id.equals(order.id)).toBe(true);
|
|
expect(retrieved?.status).toBe(expectedStatus);
|
|
expect(retrieved?.calculateTotal()).toEqual(expectedTotal);
|
|
});
|
|
});
|
|
```
|
|
|
|
El ejemplo presupone una infraestructura de pruebas que proporciona una base de datos controlada y un pedido con `status` público y `calculateTotal()`. El total esperado debe ser un valor numérico u objeto de valor inmutable; en otros modelos, captura una proyección independiente con todos los campos y líneas que el contrato exige persistir.
|
|
|
|
Para un sistema real, deben establecerse mecanismos de aislamiento y limpieza de datos entre pruebas.
|
|
|
|
### Criterios de aplicación
|
|
|
|
- Verifica las integraciones que puedan producir fallos relevantes.
|
|
- Utiliza dependencias reales cuando sea necesario comprobar su comportamiento.
|
|
- Mantén entornos reproducibles.
|
|
- Evita depender de datos de producción.
|
|
- Aísla el estado de las pruebas.
|
|
- Comprueba errores y comportamientos de integración relevantes.
|
|
- No conviertas todas las pruebas en pruebas de integración si una prueba unitaria proporciona suficiente confianza.
|
|
|
|
### Diferencia importante
|
|
|
|
Una prueba que utiliza varios objetos reales no es automáticamente una prueba de integración de infraestructura.
|
|
|
|
Del mismo modo, una prueba que simula todas las dependencias externas puede estar verificando únicamente la coordinación de un componente.
|
|
|
|
**Clasifica las pruebas por el comportamiento y los límites que comprueban, no únicamente por el número de objetos utilizados.**
|
|
|
|
---
|
|
|
|
### 2.3. Pruebas de extremo a extremo y aceptación (E2E / Acceptance Tests)
|
|
|
|
Las pruebas E2E verifican recorridos completos desde la perspectiva del usuario o consumidor del sistema.
|
|
|
|
Pueden atravesar:
|
|
|
|
- Interfaz de usuario.
|
|
- APIs.
|
|
- Autenticación.
|
|
- Aplicación.
|
|
- Dominio.
|
|
- Persistencia.
|
|
- Integraciones.
|
|
|
|
### Características habituales
|
|
|
|
- Mayor número de componentes implicados.
|
|
- Preparación más compleja.
|
|
- Posible mayor tiempo de ejecución.
|
|
- Sensibilidad a cambios de interfaz o infraestructura.
|
|
- Capacidad para detectar problemas de integración completa.
|
|
|
|
### Ejemplo
|
|
|
|
```typescript id="mq08am"
|
|
test('the user can complete checkout', async ({ page }) => {
|
|
await page.goto('/products');
|
|
|
|
await page.getByRole('button', {
|
|
name: 'Añadir al carrito',
|
|
}).click();
|
|
|
|
await page.getByRole('button', {
|
|
name: 'Finalizar compra',
|
|
}).click();
|
|
|
|
await page.getByRole('button', {
|
|
name: 'Confirmar pedido',
|
|
}).click();
|
|
|
|
await expect(
|
|
page.getByRole('heading', {
|
|
name: 'Pedido confirmado',
|
|
}),
|
|
).toBeVisible();
|
|
});
|
|
```
|
|
|
|
El ejemplo presupone un entorno de pruebas que permite confirmar pedidos sin ejecutar cargos reales.
|
|
|
|
### Buenas prácticas
|
|
|
|
- Prioriza recorridos críticos.
|
|
- Incorpora otros flujos cuando el riesgo lo justifique.
|
|
- Utiliza selectores accesibles y estables.
|
|
- Evita depender de textos o estructuras internas innecesarias cuando existan alternativas más robustas.
|
|
- Prepara datos de prueba controlados.
|
|
- Evita dependencias de servicios externos inestables.
|
|
- Utiliza entornos aislados para pagos, correos y otras operaciones sensibles.
|
|
- Comprueba resultados observables.
|
|
|
|
No introduzcas números de tarjeta, credenciales o datos reales en las pruebas.
|
|
|
|
### Pruebas de aceptación
|
|
|
|
Las pruebas de aceptación comprueban requisitos desde la perspectiva del negocio o del usuario.
|
|
|
|
Pueden implementarse mediante pruebas E2E, integración u otros niveles.
|
|
|
|
**Aceptación y E2E no son necesariamente conceptos equivalentes.**
|
|
|
|
---
|
|
|
|
## 3. Patrón Preparar-Actuar-Verificar (Arrange-Act-Assert)
|
|
|
|
El patrón AAA organiza las pruebas en tres fases:
|
|
|
|
1. **Preparar (Arrange):** establecer los datos y condiciones necesarias.
|
|
2. **Actuar (Act):** ejecutar el comportamiento.
|
|
3. **Verificar (Assert):** comprobar el resultado.
|
|
|
|
### Ejemplo
|
|
|
|
```typescript id="4e1tbg"
|
|
it('applies a 20 percent discount to premium users', () => {
|
|
// Preparar.
|
|
const user = new User({ isPremium: true });
|
|
const cart = new Cart(user);
|
|
|
|
cart.addItem({ price: 100 });
|
|
|
|
// Actuar.
|
|
const total = cart.calculateTotal();
|
|
|
|
// Verificar.
|
|
expect(total).toBe(80);
|
|
});
|
|
```
|
|
|
|
### Criterios de aplicación
|
|
|
|
- Mantén identificable la operación probada.
|
|
- Evita preparaciones extensas que oculten la intención.
|
|
- Utiliza factorías de prueba cuando simplifiquen el escenario.
|
|
- Agrupa verificaciones relacionadas.
|
|
- No introduzcas abstracciones innecesarias.
|
|
- No es obligatorio incluir comentarios AAA cuando la estructura resulte evidente.
|
|
|
|
### Escribir AAA en orden inverso
|
|
|
|
En ocasiones resulta más sencillo diseñar la prueba comenzando por el resultado esperado:
|
|
|
|
1. **Assert:** ¿qué quiero comprobar?
|
|
2. **Act:** ¿qué operación debe producirlo?
|
|
3. **Arrange:** ¿qué datos y dependencias necesito?
|
|
|
|
Este procedimiento ayuda a diseñar pruebas centradas en resultados.
|
|
|
|
No es obligatorio utilizarlo para todas las pruebas.
|
|
|
|
---
|
|
|
|
## 4. Nomenclatura de las pruebas
|
|
|
|
Los nombres deben describir comportamientos concretos y verificables.
|
|
|
|
### Nombres poco expresivos
|
|
|
|
```typescript id="d5nbzl"
|
|
it('should work correctly', () => {});
|
|
it('handles the edge case', () => {});
|
|
it('sets the data property', () => {});
|
|
```
|
|
|
|
### Nombres más adecuados
|
|
|
|
```typescript id="rx85zs"
|
|
it('calculates a 20 percent discount for premium users', () => {});
|
|
it('returns an error when the cart is empty', () => {});
|
|
it('recognizes racecar as a palindrome', () => {});
|
|
```
|
|
|
|
### Convenciones habituales
|
|
|
|
**Descripción directa del comportamiento**
|
|
|
|
```typescript id="wj1e44"
|
|
it('applies tax based on the shipping state', () => {});
|
|
```
|
|
|
|
**When-Then**
|
|
|
|
```typescript id="533rfi"
|
|
it('returns 5 when adding 2 and 3', () => {});
|
|
```
|
|
|
|
**Given-When-Then**
|
|
|
|
```typescript id="idfv3p"
|
|
describe('given a premium user', () => {
|
|
describe('when completing checkout', () => {
|
|
it('applies a 20 percent discount', () => {});
|
|
});
|
|
});
|
|
```
|
|
|
|
Las cadenas de texto utilizadas como nombres de prueba pueden redactarse en castellano cuando el proyecto lo establezca.
|
|
|
|
### Recomendaciones
|
|
|
|
- Utiliza lenguaje del dominio.
|
|
- Describe resultados observables.
|
|
- Evita referencias innecesarias a propiedades privadas.
|
|
- Mantén una convención coherente.
|
|
- No anides bloques `describe` excesivamente.
|
|
- Evita nombres que dependan de una implementación concreta.
|
|
|
|
### Una responsabilidad por prueba
|
|
|
|
Como orientación, cada prueba debe comprobar un comportamiento concreto.
|
|
|
|
Esto no implica limitarla a una única aserción.
|
|
|
|
```typescript id="jkx484"
|
|
it('creates an order with its initial state', () => {
|
|
const order = createTestOrder();
|
|
|
|
expect(order.status).toBe('pending');
|
|
expect(order.items).toHaveLength(0);
|
|
expect(order.total).toBe(0);
|
|
});
|
|
```
|
|
|
|
Las tres comprobaciones corresponden al mismo comportamiento inicial.
|
|
|
|
---
|
|
|
|
## 5. Dobles de prueba (Test Doubles)
|
|
|
|
Los dobles de prueba permiten sustituir colaboradores durante la verificación de un componente.
|
|
|
|
Su utilización debe responder a una necesidad concreta.
|
|
|
|
### 5.1. Dummy
|
|
|
|
Objeto que se proporciona porque una interfaz o constructor lo exige, pero que no interviene en el comportamiento probado.
|
|
|
|
```typescript id="k3ajk6"
|
|
const dummyLogger: Logger = {
|
|
log(): void {},
|
|
};
|
|
|
|
const service = new UserService(repository, dummyLogger);
|
|
```
|
|
|
|
Evita utilizar aserciones de tipo inseguras como `{} as Logger` cuando puedas proporcionar una implementación mínima válida.
|
|
|
|
### 5.2. Stub
|
|
|
|
Proporciona respuestas predefinidas.
|
|
|
|
```typescript id="9qsma1"
|
|
const stubRepository: UserRepository = {
|
|
async findById(_id: UserId): Promise<User | null> {
|
|
return createTestUser();
|
|
},
|
|
|
|
async save(_user: User): Promise<void> {},
|
|
};
|
|
```
|
|
|
|
Los stubs permiten controlar escenarios sin depender de infraestructura externa.
|
|
|
|
### 5.3. Spy
|
|
|
|
Registra las llamadas realizadas a una operación.
|
|
|
|
```typescript id="h0x6i6"
|
|
const sentEmails: string[] = [];
|
|
|
|
const emailSpy: EmailService = {
|
|
async send(to: string, _message: string): Promise<void> {
|
|
sentEmails.push(to);
|
|
},
|
|
};
|
|
```
|
|
|
|
Verificación:
|
|
|
|
```typescript id="1xz90c"
|
|
expect(sentEmails).toContain('user@example.com');
|
|
```
|
|
|
|
### 5.4. Mock
|
|
|
|
Permite definir o verificar interacciones esperadas.
|
|
|
|
Ejemplo con Vitest:
|
|
|
|
```typescript id="1bcpjq"
|
|
const sendMock = vi.fn(async (_to: string, _message: string) => {});
|
|
|
|
const emailService: EmailService = {
|
|
send: sendMock,
|
|
};
|
|
```
|
|
|
|
Después de ejecutar el comportamiento:
|
|
|
|
```typescript id="5qktnx"
|
|
expect(sendMock).toHaveBeenCalledWith(
|
|
'user@example.com',
|
|
'Pedido confirmado',
|
|
);
|
|
```
|
|
|
|
El ejemplo utiliza Vitest. En proyectos con Jest deben emplearse sus APIs equivalentes.
|
|
|
|
No mezcles mecanismos de Jest y Vitest indiscriminadamente.
|
|
|
|
### 5.5. Fake
|
|
|
|
Es una implementación funcional simplificada de una dependencia.
|
|
|
|
```typescript id="88n8bh"
|
|
class InMemoryUserRepository implements UserRepository {
|
|
private readonly users = new Map<string, User>();
|
|
|
|
async save(user: User): Promise<void> {
|
|
this.users.set(user.id.value, user);
|
|
}
|
|
|
|
async findById(id: UserId): Promise<User | null> {
|
|
return this.users.get(id.value) ?? null;
|
|
}
|
|
}
|
|
```
|
|
|
|
Esta implementación puede permitir pruebas rápidas sin utilizar una base de datos real.
|
|
|
|
### Criterios para seleccionar dobles
|
|
|
|
- Utiliza objetos reales cuando sean sencillos y deterministas.
|
|
- Utiliza stubs para controlar respuestas.
|
|
- Utiliza spies o mocks cuando las interacciones formen parte del comportamiento relevante.
|
|
- Utiliza fakes cuando una implementación simplificada facilite las pruebas.
|
|
- Evita simular detalles internos que no pertenecen al contrato.
|
|
- No introduzcas mocks únicamente para alcanzar un aislamiento artificial.
|
|
|
|
**Una prueba con demasiadas simulaciones puede verificar únicamente las expectativas del propio test, sin proporcionar confianza suficiente sobre el sistema real.**
|
|
|
|
---
|
|
|
|
## 6. Estrategia de pruebas por responsabilidad arquitectónica
|
|
|
|
La estrategia debe adaptarse al diseño del proyecto.
|
|
|
|
### 6.1. Dominio
|
|
|
|
Comprueba:
|
|
|
|
- Reglas de negocio.
|
|
- Objetos de valor.
|
|
- Entidades.
|
|
- Invariantes.
|
|
- Cálculos.
|
|
- Transiciones de estado.
|
|
|
|
Las pruebas unitarias suelen resultar apropiadas cuando el dominio puede ejecutarse sin infraestructura externa.
|
|
|
|
```typescript id="jxbk58"
|
|
describe('Money', () => {
|
|
it('adds amounts with the same currency', () => {
|
|
const first = Money.euros(10);
|
|
const second = Money.euros(20);
|
|
|
|
const result = first.add(second);
|
|
|
|
expect(result.equals(Money.euros(30))).toBe(true);
|
|
});
|
|
|
|
it('rejects amounts with different currencies', () => {
|
|
const euros = Money.euros(10);
|
|
const dollars = Money.dollars(10);
|
|
|
|
expect(() => euros.add(dollars)).toThrow(CurrencyMismatchError);
|
|
});
|
|
});
|
|
```
|
|
|
|
### 6.2. Aplicación
|
|
|
|
Comprueba:
|
|
|
|
- Coordinación de casos de uso.
|
|
- Interacciones entre colaboradores.
|
|
- Políticas de aplicación.
|
|
- Tratamiento de errores.
|
|
- Resultados observables.
|
|
|
|
```typescript id="7bzp6w"
|
|
describe('CreateOrderUseCase', () => {
|
|
it('persists a valid order', async () => {
|
|
const repository = new InMemoryOrderRepository();
|
|
const useCase = new CreateOrderUseCase(repository);
|
|
|
|
const result = await useCase.execute(
|
|
createValidOrderRequest(),
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(await repository.count()).toBe(1);
|
|
});
|
|
});
|
|
```
|
|
|
|
No todas las pruebas de aplicación necesitan simular infraestructura.
|
|
|
|
### 6.3. Infraestructura
|
|
|
|
Comprueba:
|
|
|
|
- Persistencia.
|
|
- Consultas.
|
|
- Adaptadores.
|
|
- Serialización.
|
|
- Integraciones.
|
|
- Transacciones.
|
|
- Comportamientos específicos de tecnología.
|
|
|
|
```typescript id="gu3tuw"
|
|
describe('PostgresOrderRepository', () => {
|
|
it('persists and retrieves an order', async () => {
|
|
const repository = createTestPostgresRepository();
|
|
const order = createTestOrder();
|
|
const expectedStatus = order.status;
|
|
const expectedTotal = order.calculateTotal();
|
|
|
|
await repository.save(order);
|
|
|
|
const retrieved = await repository.findById(order.id);
|
|
|
|
expect(retrieved?.id.equals(order.id)).toBe(true);
|
|
expect(retrieved?.status).toBe(expectedStatus);
|
|
expect(retrieved?.calculateTotal()).toEqual(expectedTotal);
|
|
});
|
|
});
|
|
```
|
|
|
|
Para pruebas reales, el repositorio debe utilizar una conexión controlada y un mecanismo adecuado de aislamiento de datos. Este ejemplo presupone el mismo contrato ilustrativo de pedido (`status` y `calculateTotal()`) descrito en la sección 2.2; amplía la comparación a todos los campos y líneas persistidos del modelo real.
|
|
|
|
### 6.4. Frontend
|
|
|
|
Comprueba según corresponda:
|
|
|
|
- Renderizado y comportamiento de componentes.
|
|
- Interacción del usuario.
|
|
- Formularios.
|
|
- Validaciones.
|
|
- Accesibilidad.
|
|
- Gestión de estados.
|
|
- Integración con APIs.
|
|
- Flujos críticos.
|
|
|
|
Prioriza pruebas que interactúen mediante comportamientos observables y no mediante detalles internos del framework.
|
|
|
|
### 6.5. Código compartido
|
|
|
|
Comprueba:
|
|
|
|
- Contratos.
|
|
- Esquemas.
|
|
- Serialización.
|
|
- Validaciones.
|
|
- Compatibilidad.
|
|
- Transformaciones.
|
|
- Invariantes comunes.
|
|
|
|
La reutilización de un tipo entre frontend y backend no garantiza que los datos recibidos cumplan dicho tipo durante la ejecución.
|
|
|
|
---
|
|
|
|
## 7. Pruebas de integración de alto valor
|
|
|
|
Prioriza las pruebas de integración en los límites donde los errores puedan tener consecuencias importantes.
|
|
|
|
### 7.1. Límites entre sistemas
|
|
|
|
Ejemplos:
|
|
|
|
- API y base de datos.
|
|
- Aplicación y cola.
|
|
- Servicio y proveedor externo.
|
|
- Serialización y validación.
|
|
- Autenticación y autorización.
|
|
- Persistencia y transacciones.
|
|
|
|
### 7.2. Procesos críticos
|
|
|
|
Presta especial atención a:
|
|
|
|
- Pagos.
|
|
- Facturación.
|
|
- Autorización.
|
|
- Integridad de datos.
|
|
- Operaciones irreversibles.
|
|
- Procesos empresariales esenciales.
|
|
|
|
### 7.3. Consultas complejas
|
|
|
|
Comprueba consultas que incluyan:
|
|
|
|
- Relaciones complejas.
|
|
- Agregaciones.
|
|
- Filtros relevantes.
|
|
- Restricciones de integridad.
|
|
- Paginación.
|
|
- Concurrencia.
|
|
- Comportamientos específicos del motor.
|
|
|
|
No presupongas que una consulta funciona correctamente únicamente porque su lógica parece adecuada al inspeccionar el código.
|
|
|
|
---
|
|
|
|
## 8. Pruebas de contrato (Contract Tests)
|
|
|
|
Las pruebas de contrato permiten comprobar que diferentes implementaciones respetan un comportamiento común.
|
|
|
|
Son especialmente útiles para:
|
|
|
|
- Repositorios.
|
|
- Adaptadores.
|
|
- Proveedores externos.
|
|
- Implementaciones intercambiables.
|
|
- APIs entre aplicaciones.
|
|
- Contratos compartidos entre frontend y backend.
|
|
|
|
### Ejemplo de contrato compartido
|
|
|
|
El ejemplo presupone que `User` expone `id: UserId`, `name: string` y `email: string`, y que `createTestUser(overrides)` crea identificadores únicos y respeta los datos recibidos. Son contratos ilustrativos: adapta las proyecciones a las propiedades públicas o valores del modelo real. Captura los valores esperados antes de guardar; comprobar solo la identidad no detecta la pérdida de otros campos.
|
|
|
|
```typescript id="5h26e3"
|
|
function testUserRepositoryContract(
|
|
createRepository: () => Promise<UserRepository>,
|
|
resetRepository: () => Promise<void>,
|
|
): void {
|
|
describe('UserRepository contract', () => {
|
|
let repository: UserRepository;
|
|
|
|
beforeEach(async () => {
|
|
await resetRepository();
|
|
repository = await createRepository();
|
|
});
|
|
|
|
it('persists and retrieves a user', async () => {
|
|
const expectedName = 'Contract User 47';
|
|
const expectedEmail = 'contract-47@example.com';
|
|
const user = createTestUser({
|
|
name: expectedName,
|
|
email: expectedEmail,
|
|
});
|
|
|
|
await repository.save(user);
|
|
|
|
const retrieved = await repository.findById(user.id);
|
|
|
|
expect(retrieved).not.toBeNull();
|
|
expect(retrieved?.id.equals(user.id)).toBe(true);
|
|
expect(retrieved?.name).toBe(expectedName);
|
|
expect(retrieved?.email).toBe(expectedEmail);
|
|
});
|
|
|
|
it('keeps the persisted data of different users independent', async () => {
|
|
const first = createTestUser({
|
|
name: 'First Contract User',
|
|
email: 'first-contract@example.com',
|
|
});
|
|
const second = createTestUser({
|
|
name: 'Second Contract User',
|
|
email: 'second-contract@example.com',
|
|
});
|
|
|
|
await repository.save(first);
|
|
await repository.save(second);
|
|
|
|
const retrievedFirst = await repository.findById(first.id);
|
|
const retrievedSecond = await repository.findById(second.id);
|
|
|
|
expect(retrievedFirst?.id.equals(first.id)).toBe(true);
|
|
expect(retrievedFirst?.name).toBe('First Contract User');
|
|
expect(retrievedFirst?.email).toBe('first-contract@example.com');
|
|
expect(retrievedSecond?.id.equals(second.id)).toBe(true);
|
|
expect(retrievedSecond?.name).toBe('Second Contract User');
|
|
expect(retrievedSecond?.email).toBe('second-contract@example.com');
|
|
});
|
|
|
|
it('returns null when the user does not exist', async () => {
|
|
const missingId = createMissingUserId();
|
|
|
|
const retrieved = await repository.findById(missingId);
|
|
|
|
expect(retrieved).toBeNull();
|
|
});
|
|
});
|
|
}
|
|
```
|
|
|
|
Aplicación a distintas implementaciones:
|
|
|
|
```typescript id="2ov5k9"
|
|
testUserRepositoryContract(
|
|
async () => new InMemoryUserRepository(),
|
|
async () => resetInMemoryRepository(),
|
|
);
|
|
```
|
|
|
|
```typescript id="hhpi1w"
|
|
testUserRepositoryContract(
|
|
async () => createPostgresUserRepository(),
|
|
async () => resetTestDatabase(),
|
|
);
|
|
```
|
|
|
|
### Criterios importantes
|
|
|
|
- Verifica comportamientos, no únicamente firmas.
|
|
- Comprueba resultados y errores esperados.
|
|
- Mantén las pruebas independientes.
|
|
- Utiliza una estrategia de limpieza adecuada.
|
|
- Comprueba las garantías compartidas por todas las implementaciones.
|
|
- Si `save` permite actualizar usuarios, añade una prueba que cambie un campo, guarde y lo recupere de nuevo. No presupongas una semántica de actualización que el contrato no haya definido.
|
|
- No presupongas que una implementación en memoria reproduce exactamente el comportamiento de una base de datos real.
|
|
|
|
### Contratos entre servicios
|
|
|
|
Cuando existan APIs entre frontend y backend o entre servicios, considera:
|
|
|
|
- Validación de esquemas.
|
|
- Compatibilidad de versiones.
|
|
- Campos obligatorios y opcionales.
|
|
- Errores contractuales.
|
|
- Formatos de fecha y números.
|
|
- Identificadores.
|
|
- Cambios compatibles e incompatibles.
|
|
|
|
Las pruebas de contrato deben complementar, no sustituir automáticamente, las pruebas de integración relevantes.
|
|
|
|
---
|
|
|
|
## 9. Constructores de datos de prueba (Test Builders)
|
|
|
|
Los *Test Builders* facilitan preparar objetos complejos para escenarios de prueba.
|
|
|
|
### Cuándo utilizarlos
|
|
|
|
- Los objetos requieren numerosos datos.
|
|
- Existen valores predeterminados útiles.
|
|
- Los escenarios necesitan variaciones frecuentes.
|
|
- La preparación se repite en numerosas pruebas.
|
|
|
|
### Ejemplo
|
|
|
|
```typescript id="2cdvvu"
|
|
interface OrderProps {
|
|
id: string;
|
|
customerId: string;
|
|
items: Item[];
|
|
status: 'pending' | 'paid';
|
|
}
|
|
```
|
|
|
|
```typescript id="qj5fhj"
|
|
class OrderBuilder {
|
|
private id = 'order-1';
|
|
private customerId = 'customer-1';
|
|
private items: Item[] = [];
|
|
private status: OrderProps['status'] = 'pending';
|
|
|
|
withId(id: string): this {
|
|
this.id = id;
|
|
return this;
|
|
}
|
|
|
|
withItems(items: Item[]): this {
|
|
this.items = [...items];
|
|
return this;
|
|
}
|
|
|
|
paid(): this {
|
|
this.status = 'paid';
|
|
return this;
|
|
}
|
|
|
|
build(): Order {
|
|
return Order.create({
|
|
id: this.id,
|
|
customerId: this.customerId,
|
|
items: [...this.items],
|
|
status: this.status,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
Uso:
|
|
|
|
```typescript id="xyal0i"
|
|
const order = new OrderBuilder()
|
|
.withItems([{ sku: 'ABC', price: 100 }])
|
|
.paid()
|
|
.build();
|
|
```
|
|
|
|
### Buenas prácticas
|
|
|
|
- Proporciona datos predeterminados válidos.
|
|
- Permite modificar únicamente los campos relevantes del escenario.
|
|
- Evita compartir referencias mutables entre pruebas.
|
|
- No utilices aserciones de tipo para ocultar datos obligatorios ausentes.
|
|
- Mantén los builders sencillos.
|
|
- No reproduzcas toda la lógica de producción dentro del builder.
|
|
|
|
### Alternativa mediante factorías
|
|
|
|
Para objetos sencillos, una función suele ser suficiente:
|
|
|
|
```typescript id="mxnt4u"
|
|
function createTestUser(
|
|
overrides: Partial<UserProps> = {},
|
|
): User {
|
|
return User.create({
|
|
name: 'Test User',
|
|
email: 'test@example.com',
|
|
...overrides,
|
|
});
|
|
}
|
|
```
|
|
|
|
No introduzcas una clase Builder cuando una factoría de prueba clara resuelva adecuadamente el problema.
|
|
|
|
---
|
|
|
|
## 10. Aislamiento y fiabilidad
|
|
|
|
Una prueba debe producir resultados reproducibles bajo las mismas condiciones.
|
|
|
|
### 10.1. Evitar estado compartido
|
|
|
|
No permitas que una prueba dependa del orden de ejecución de otras.
|
|
|
|
- Reinicia datos entre pruebas.
|
|
- Evita variables globales mutables compartidas.
|
|
- Limpia los recursos creados.
|
|
- Utiliza instancias independientes cuando corresponda.
|
|
|
|
### 10.2. Controlar tiempo y aleatoriedad
|
|
|
|
Cuando el comportamiento dependa de:
|
|
|
|
- Fecha actual.
|
|
- Zona horaria.
|
|
- Temporizadores.
|
|
- Valores aleatorios.
|
|
- Identificadores generados.
|
|
|
|
Utiliza mecanismos controlados para reproducir el escenario.
|
|
|
|
No dependas del reloj real cuando pueda producir resultados inestables.
|
|
|
|
### 10.3. Controlar concurrencia
|
|
|
|
Las pruebas paralelas pueden interferir si comparten:
|
|
|
|
- Base de datos.
|
|
- Archivos.
|
|
- Puertos.
|
|
- Variables globales.
|
|
- Recursos externos.
|
|
|
|
Define mecanismos de aislamiento adecuados.
|
|
|
|
### 10.4. Gestionar recursos
|
|
|
|
Libera correctamente:
|
|
|
|
- Conexiones.
|
|
- Servidores.
|
|
- Archivos temporales.
|
|
- Suscripciones.
|
|
- Temporizadores.
|
|
- Procesos externos.
|
|
|
|
Utiliza los mecanismos de preparación y limpieza proporcionados por el framework de pruebas.
|
|
|
|
### 10.5. Evitar pruebas inestables (Flaky Tests)
|
|
|
|
Una prueba inestable puede pasar o fallar sin cambios relevantes en el código.
|
|
|
|
Causas habituales:
|
|
|
|
- Condiciones de carrera.
|
|
- Esperas temporales arbitrarias.
|
|
- Dependencias externas.
|
|
- Datos compartidos.
|
|
- Orden de ejecución.
|
|
- Operaciones asíncronas no esperadas.
|
|
|
|
No soluciones la inestabilidad aumentando indiscriminadamente los tiempos de espera.
|
|
|
|
Identifica y corrige la causa.
|
|
|
|
**Una prueba inestable reduce la confianza en toda la batería de pruebas.**
|
|
|
|
---
|
|
|
|
## 11. Pruebas asíncronas
|
|
|
|
En aplicaciones TypeScript y Node.js, las pruebas deben esperar correctamente las operaciones asíncronas.
|
|
|
|
### Ejemplo incorrecto
|
|
|
|
```typescript id="f31do9"
|
|
it('saves the order', () => {
|
|
orderRepository.save(order);
|
|
|
|
expect(orderRepository.count()).toBe(1);
|
|
});
|
|
```
|
|
|
|
La operación puede continuar ejecutándose después de la aserción.
|
|
|
|
### Alternativa
|
|
|
|
```typescript id="3hz4v4"
|
|
it('saves the order', async () => {
|
|
await orderRepository.save(order);
|
|
|
|
const count = await orderRepository.count();
|
|
|
|
expect(count).toBe(1);
|
|
});
|
|
```
|
|
|
|
### Recomendaciones
|
|
|
|
- Espera las promesas relevantes.
|
|
- No ignores rechazos.
|
|
- Evita temporizadores arbitrarios.
|
|
- Gestiona correctamente la limpieza asíncrona.
|
|
- Utiliza mecanismos de espera basados en condiciones cuando corresponda.
|
|
- Comprueba errores mediante aserciones apropiadas.
|
|
|
|
Ejemplo:
|
|
|
|
```typescript id="8wzl6r"
|
|
await expect(
|
|
orderRepository.save(invalidOrder),
|
|
).rejects.toThrow(InvalidOrderError);
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Pruebas de regresión
|
|
|
|
Cuando se corrige un defecto reproducible, conviene añadir una prueba que permita detectar su reaparición.
|
|
|
|
### Procedimiento recomendado
|
|
|
|
1. Reproduce el problema.
|
|
2. Identifica el comportamiento esperado.
|
|
3. Escribe una prueba que falle por el defecto, cuando sea viable.
|
|
4. Corrige la implementación.
|
|
5. Comprueba que la prueba pasa.
|
|
6. Ejecuta las verificaciones relacionadas.
|
|
|
|
### Código legado
|
|
|
|
Cuando no exista infraestructura adecuada:
|
|
|
|
- Analiza el comportamiento actual.
|
|
- Utiliza pruebas de caracterización cuando resulte posible.
|
|
- Mantén los cambios acotados.
|
|
- Comprueba los contratos afectados.
|
|
- Añade pruebas de regresión cuando sea viable.
|
|
|
|
No utilices la ausencia de pruebas como una razón automática para impedir correcciones necesarias.
|
|
|
|
---
|
|
|
|
## 13. Cobertura de pruebas
|
|
|
|
La cobertura indica qué partes del código han sido ejecutadas durante las pruebas.
|
|
|
|
Puede ayudar a detectar áreas poco verificadas, pero no demuestra que los comportamientos sean correctos.
|
|
|
|
### Consideraciones
|
|
|
|
- Una cobertura elevada no garantiza pruebas de calidad.
|
|
- Una cobertura baja puede indicar riesgos relevantes.
|
|
- No escribas pruebas sin significado únicamente para aumentar porcentajes.
|
|
- Prioriza reglas de negocio, casos límite y procesos críticos.
|
|
- Evalúa la cobertura junto con la calidad de las aserciones y los escenarios.
|
|
|
|
### Métricas habituales
|
|
|
|
- Cobertura de líneas.
|
|
- Cobertura de funciones.
|
|
- Cobertura de ramas.
|
|
- Cobertura de instrucciones.
|
|
|
|
Las métricas pueden utilizarse como indicadores de revisión.
|
|
|
|
No establezcas umbrales universales sin considerar el tipo de sistema y los criterios específicos del proyecto.
|
|
|
|
---
|
|
|
|
## 14. Errores habituales en las pruebas
|
|
|
|
| Error | Problema | Recomendación |
|
|
|---|---|---|
|
|
| Probar detalles internos | Pruebas frágiles ante refactorizaciones correctas. | Verificar comportamientos observables. |
|
|
| Utilizar demasiados mocks | Las pruebas reproducen expectativas artificiales. | Utilizar objetos reales cuando corresponda. |
|
|
| Compartir estado | Resultados dependientes del orden de ejecución. | Aislar escenarios. |
|
|
| No incluir aserciones relevantes | Falsa confianza. | Verificar resultados significativos. |
|
|
| Probar únicamente código trivial | Bajo valor respecto al esfuerzo. | Priorizar lógica y riesgos. |
|
|
| Pruebas excesivamente lentas | Retroalimentación tardía. | Optimizar aislamiento y ejecución. |
|
|
| Dependencias externas inestables | Fallos no deterministas. | Utilizar entornos controlados. |
|
|
| Datos de producción | Riesgos de privacidad e integridad. | Utilizar datos sintéticos o preparados. |
|
|
| Modificar expectativas para hacer pasar pruebas | Ocultación de defectos. | Investigar el comportamiento correcto. |
|
|
| Eliminar pruebas que fallan | Pérdida de protección frente a regresiones. | Analizar y corregir la causa. |
|
|
| No esperar operaciones asíncronas | Resultados inconsistentes. | Utilizar correctamente `async/await`. |
|
|
| Aplicar una pirámide rígida | Pruebas inadecuadas para la arquitectura. | Ajustar la estrategia a los riesgos reales. |
|
|
|
|
---
|
|
|
|
## 15. Disciplina de pruebas para agentes de IA
|
|
|
|
Al modificar código o pruebas:
|
|
|
|
- No cambies expectativas únicamente para conseguir que las pruebas pasen.
|
|
- No elimines pruebas que fallen sin analizar su causa.
|
|
- No desactives validaciones ni controles de seguridad para simplificar un escenario.
|
|
- No introduzcas mocks que oculten comportamientos incorrectos.
|
|
- No cambies herramientas de pruebas sin una necesidad justificada.
|
|
- Respeta las convenciones y frameworks existentes.
|
|
- Limita los cambios a los archivos relacionados con la tarea.
|
|
- Añade pruebas de regresión cuando corrijas defectos reproducibles y resulte viable.
|
|
- No inventes resultados de ejecución.
|
|
- No declares que una prueba ha pasado si no se ha ejecutado.
|
|
|
|
### Cuando una prueba existente falla
|
|
|
|
1. Comprueba si el fallo ya existía.
|
|
2. Analiza el comportamiento esperado.
|
|
3. Identifica si existe un defecto funcional o una expectativa obsoleta.
|
|
4. Corrige el código o la prueba según corresponda.
|
|
5. Justifica cualquier cambio contractual.
|
|
6. Ejecuta nuevamente las verificaciones pertinentes.
|
|
|
|
### Cuando no pueden ejecutarse pruebas
|
|
|
|
Indica:
|
|
|
|
- Qué verificaciones no se ejecutaron.
|
|
- Por qué no pudieron ejecutarse.
|
|
- Qué otras comprobaciones se realizaron.
|
|
- Qué riesgos o incertidumbres permanecen.
|
|
|
|
**La transparencia sobre las verificaciones realizadas forma parte de la calidad de la entrega.**
|
|
|
|
---
|
|
|
|
## 16. Lista de comprobación de pruebas
|
|
|
|
### Diseño
|
|
|
|
- [ ] ¿Qué comportamiento necesito verificar?
|
|
- [ ] ¿Qué nivel de prueba resulta más apropiado?
|
|
- [ ] ¿La prueba aporta confianza real?
|
|
- [ ] ¿Estoy evitando comprobar detalles internos innecesarios?
|
|
- [ ] ¿El escenario representa un requisito o riesgo relevante?
|
|
|
|
### Preparación
|
|
|
|
- [ ] ¿Los datos de prueba son válidos y controlados?
|
|
- [ ] ¿Las dependencias están correctamente aisladas?
|
|
- [ ] ¿He evitado estado compartido?
|
|
- [ ] ¿Las operaciones asíncronas se esperan correctamente?
|
|
- [ ] ¿Los recursos se limpian al finalizar?
|
|
|
|
### Verificación
|
|
|
|
- [ ] ¿Las aserciones comprueban el comportamiento esperado?
|
|
- [ ] ¿Los casos límite relevantes están cubiertos?
|
|
- [ ] ¿Los errores esperados se verifican correctamente?
|
|
- [ ] ¿La prueba es determinista?
|
|
- [ ] ¿He evitado simulaciones innecesarias?
|
|
|
|
### Integración
|
|
|
|
- [ ] ¿Los contratos relevantes están comprobados?
|
|
- [ ] ¿Las integraciones críticas disponen de pruebas adecuadas?
|
|
- [ ] ¿Los datos y recursos externos están aislados?
|
|
- [ ] ¿Las implementaciones alternativas respetan los contratos?
|
|
- [ ] ¿Se han considerado transacciones y concurrencia cuando corresponde?
|
|
|
|
### Finalización
|
|
|
|
- [ ] ¿Se han ejecutado las pruebas relacionadas con los cambios?
|
|
- [ ] ¿Se han ejecutado las comprobaciones adicionales necesarias?
|
|
- [ ] ¿Las pruebas existentes siguen siendo válidas?
|
|
- [ ] ¿Se han incorporado pruebas de regresión cuando corresponde?
|
|
- [ ] ¿He comunicado las verificaciones no realizadas?
|
|
- [ ] ¿Puedo afirmar con precisión qué comportamiento se ha verificado?
|
|
|
|
---
|
|
|
|
## Principio final
|
|
|
|
> "Una buena prueba proporciona confianza sobre un comportamiento relevante y permite modificar el software sin introducir regresiones inesperadas."
|
|
|
|
Utiliza pruebas unitarias, de integración, de contrato y E2E según las necesidades del sistema.
|
|
|
|
Prioriza escenarios significativos, resultados observables y mecanismos reproducibles.
|
|
|
|
No conviertas la cobertura, el número de pruebas o la estructura de la pirámide en objetivos independientes de la calidad del software.
|
|
|
|
**La mejor estrategia de pruebas es la que proporciona confianza suficiente en los comportamientos importantes con un coste razonable de ejecución y mantenimiento.** |