Pruebas con Enterprise Node.js: Nomenclatura AAA, cobertura y fábricas de pruebas
Aprenda cómo estructurar las pruebas de Node.js con el patrón AAA, equilibrar la cobertura de pruebas unitarias e integración, verificar cinco resultados clave del dominio y aislar las pruebas mediante fábricas.
Incluso la base de código más bien estructurada se deteriorará con el tiempo si no hay algo que la verifique automáticamente. A medida que los equipos crecen y el conjunto de funcionalidades sigue expandiéndose, un conjunto sólido de pruebas se convierte en la principal línea de defensa contra las regresiones que podrían llegar a la producción.
Esta sección se centra en prácticas de pruebas de nivel empresarial para Node.js: cómo escribir pruebas que sean fáciles de entender, cómo mantener las pruebas independientes entre sí, cómo encontrar el equilibrio adecuado entre la cobertura de pruebas unitarias e integración, y cómo confirmar los cinco resultados clave que cualquier parte de la lógica del dominio puede generar.
1. Estructura y nombrado de las pruebas: el patrón AAA
Considere sus pruebas como una documentación viva de las reglas de negocio que cubren. Cuando una falla dentro de un pipeline CI/CD a medianoche, quien esté de guardia debe comprender de inmediato qué falló, bajo qué circunstancias y qué debería haber sucedido en su lugar.
El patrón AAA (Arrange-Act-Assert)
Estructure cada prueba de modo que se divida en tres etapas claramente separadas:
┌─────────────────────────────────────────────────────────┐
│ 1. ARRANGE │
│ Set up preconditions, create inputs, mock dependencies│
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 2. ACT │
│ Execute the single domain operation being tested │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 3. ASSERT │
│ Verify returned results, DB state, and side-effects │
└─────────────────────────────────────────────────────────┘
Estándar descriptivo para nombrar pruebas
Evite etiquetas vagas como it('works') o it('should test user service'). En su lugar, adopte una convención de nomenclatura que indique desde el principio la intención de la prueba:
given [precondición/contexto], when [acción], then [resultado esperado]
La forma incorrecta: definiciones de pruebas enigmáticas
// orders.service.test.js
describe('orders service', () => {
it('creates order', async () => {
// Everything mashed together, no context
const res = await orderService.createOrder({ userId: '123', items: [] });
expect(res).toBeDefined();
});
});
La forma correcta: pruebas AAA autodocumentadas
// components/orders/orders.service.test.js
const { orderService } = require('./orders.service');
const { ValidationError } = require('../../shared/errors/AppError');
const userFactory = require('../../../test/factories/user.factory');
describe('OrderService.createOrder', () => {
it('given an empty cart, when creating an order, then it throws a ValidationError', async () => {
// ARRANGE
const user = await userFactory.build();
const payload = { userId: user.id, items: [] };
// ACT & ASSERT
await expect(orderService.createOrder(payload))
.rejects
.toThrow(ValidationError);
});
});
2. Pruebas unitarias vs. de integración: encontrar el equilibrio adecuado
Una pregunta recurrente en las pruebas de Node.js es cómo distribuir los esfuerzos entre las pruebas unitarias, que se ejecutan rápidamente y dependen en gran medida de simulaciones, y las pruebas de integración, que se ejecutan más lentamente pero utilizan bases de datos reales y llamadas a redes.
┌──────────────────────────┐
│ End-to-End (E2E) │ ~10% of tests
│ (Full stack / Cypress) │ (Slowest, high confidence)
└────────────┬─────────────┘
│
┌────────────┴─────────────┐
│ Integration Tests │ ~40% of tests
│ (Real DB / HTTP Endpoints│ (Medium speed, real wiring)
└────────────┬─────────────┘
│
┌────────────┴─────────────┐
│ Unit Tests │ ~50% of tests
│ (Isolated Domain Logic) │ (Fastest, instant feedback)
└──────────────────────────┘
Pruebas unitarias: lógica de dominio pura
Las pruebas unitarias tienen como objetivo probar la lógica de negocio que se encuentra dentro de los servicios de dominio en total aislamiento. Todo aquello que interactúa con el exterior, como los repositorios de base de datos o los clientes de API externos, debe reemplazarse por simulaciones o stubs.
- Qué cubren: servicios de dominio, lógica de cálculo y ayudantes de utilidad compartidos.
- Cómo se ejecutan rápidamente: típicamente en menos de un milisegundo por prueba.
Pruebas de integración: Conexiones reales e infraestructura
Las pruebas de integración confirman que su código funciona correctamente con sistemas externos reales (PostgreSQL, Redis, RabbitMQ) y con el framework web que está utilizando (Express o Fastify).
- Qué abarcan: consultas al repositorio, puntos de extremo de API (a través de
supertest) y consumidores de colas. - Cuán rápido se ejecutan: aproximadamente de decenas a cientos de milisegundos cada una.
- En qué se basan: en bases de datos contenerizadas, creadas con herramientas como Testcontainers o Docker Compose, para así verificar la ejecución real de SQL o NoSQL.
3. Los 5 resultados clave de los servicios de dominio
Cuando se escriben pruebas unitarias o de integración para un método de servicio empresarial, esa función del dominio puede generar hasta cinco tipos diferentes de resultados. Un conjunto de pruebas completo debe abarcar cada resultado aplicable a la operación que se está probando:
┌────────────────────────────────────────────────────────────────────────┐
│ Domain Service Operation │
└──────┬──────────────┬──────────────┬──────────────────┬────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐┌─────────────┐┌─────────────┐┌─────────────────────┐┌──────────────┐
│ 1. Return ││ 2. State ││ 3. Outgoing ││ 4. Events Published ││ 5. Telemetry │
│ Value ││ Changes ││ API Calls ││ (Broker / Queue) ││ / Logs │
└─────────────┘└─────────────┘└─────────────┘└─────────────────────┘└──────────────┘
Ejemplo: Pruebas de los 5 resultados
Considere cómo probaría una operación completa del dominio como orderService.checkoutOrder.
// components/orders/orders.service.test.js
describe('OrderService.checkoutOrder', () => {
it('given a valid order, when checkout occurs, then fulfills all 5 outcomes', async () => {
// -------------------------------------------------------------
// ARRANGE: Setup Mocks & Dependencies
// -------------------------------------------------------------
const mockOrderRepo = {
findById: jest.fn().mockResolvedValue({ id: 'ord_123', status: 'PENDING', total: 100 }),
updateStatus: jest.fn().mockResolvedValue({ id: 'ord_123', status: 'PAID', total: 100 })
};
const mockPaymentGateway = {
charge: jest.fn().mockResolvedValue({ transactionId: 'txn_999', success: true })
};
const mockEventBus = {
publish: jest.fn().mockResolvedValue(true)
};
const mockLogger = {
info: jest.fn()
};
const orderService = createOrderService({
orderRepo: mockOrderRepo,
paymentGateway: mockPaymentGateway,
eventBus: mockEventBus,
logger: mockLogger
});
// -------------------------------------------------------------
// ACT: Execute Domain Action
// -------------------------------------------------------------
const result = await orderService.checkoutOrder({ orderId: 'ord_123', paymentToken: 'tok_visa' });
// -------------------------------------------------------------
// ASSERT: Verify All 5 Outcomes
// -------------------------------------------------------------
// Outcome 1: Verify Return Value
expect(result).toEqual(expect.objectContaining({
id: 'ord_123',
status: 'PAID'
}));
// Outcome 2: Verify Database State Change
expect(mockOrderRepo.updateStatus).toHaveBeenCalledWith('ord_123', 'PAID');
// Outcome 3: Verify Outgoing Third-Party Call
expect(mockPaymentGateway.charge).toHaveBeenCalledWith({
amount: 100,
token: 'tok_visa'
});
// Outcome 4: Verify Message Queue / Event Emission
expect(mockEventBus.publish).toHaveBeenCalledWith(
'order.completed',
expect.objectContaining({ orderId: 'ord_123' })
);
// Outcome 5: Verify Telemetry / Observability
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringContaining('Order ord_123 successfully checked out')
);
});
});
4. Evitar la interdependencia entre pruebas con fábricas de pruebas
Una de las causas principales de conjuntos de pruebas poco fiables es el estado mutable compartido: pruebas que dependen de configuraciones globales, filas compartidas en una base de datos o restos de una ejecución anterior de pruebas.
El problema con las configuraciones compartidas
// ❌ BAD: Hardcoded shared static data across test files
const testUser = { id: '123', email: 'john@example.com' };
// If Test A mutates testUser.email, Test B fails unpredictably!
La solución: fábricas de pruebas dinámicas
La solución consiste en utilizar fábricas de pruebas que generen datos nuevos y únicos en cada llamada, en lugar de reutilizar objetos estáticos entre archivos.
// test/factories/user.factory.js
const { crypto } = require('crypto');
class UserFactory {
static build(overrides = {}) {
const randomId = Math.random().toString(36).substring(7);
return {
id: `usr_${randomId}`,
email: `user_${randomId}@example.com`,
role: 'CUSTOMER',
createdAt: new Date(),
...overrides // Allow callers to override specific properties
};
}
static async create(dbClient, overrides = {}) {
const user = this.build(overrides);
await dbClient.query(
'INSERT INTO users (id, email, role, created_at) VALUES ($1, $2, $3, $4)',
[user.id, user.email, user.role, user.createdAt]
);
return user;
}
}
module.exports = UserFactory;
Uso en pruebas de integración
Con este enfoque, cada prueba cuenta con su propio conjunto de datos aislado, lo que permite que ejecutores de pruebas como Jest, Vitest o el ejecutor de pruebas integrado de Node ejecuten las especificaciones en paralelo sin enfrentarse a condiciones de carrera:
it('given an admin user, when fetching reports, then returns data', async () => {
// Generates fresh, isolated database record with admin role override
const adminUser = await UserFactory.create(dbClient, { role: 'ADMIN' });
const response = await request(app)
.get('/api/v1/reports')
.set('x-user-id', adminUser.id);
expect(response.status).toBe(200);
});
Lista de verificación de arquitectura para la parte 3
Antes de considerar que su configuración de pruebas está completa, revise su código contra la siguiente lista de verificación:
- Estructura AAA: ¿Está cada prueba claramente dividida en las secciones Arrange, Act y Assert?
- Nomenclatura descriptiva: ¿Los títulos de las pruebas describen el contexto, la acción y el resultado esperado, siguiendo un formato de
given... when... then...?
Lecturas relacionadas
- Sustituyendo Jest con el ejecutor de pruebas nativo de Node en Node 24 — Una migración real muestra cómo el ejecutor de pruebas integrado en Node 24 y el soporte nativo para TypeScript reducen el tiempo de CI al mismo tiempo que eliminan cuatro dependencias.
- Diseño de API en Node.js con capas: de controladores complejos a arquitectura limpia — Aprenda cómo refactorizar una API de Node.js en capas de controlador, servicio y acceso a datos para solucionar la lógica empresarial enredada, los errores inconsistentes y las dificultades de escalado.