Lo que aprenderas en este tutorial
- Configuracion de Jest y Supertest
- Pruebas de endpoints GET
- Pruebas de endpoints POST
- Pruebas de APIs que requieren autenticacion
- Mocking de bases de datos
- Mejores practicas de pruebas
Requisitos previos: Node.js 18 o superior, npm/yarn/pnpm instalado. Tener conocimientos basicos de Express facilitara la comprension.
Que son las pruebas de software? Por que son necesarias?
Historia de las pruebas
El concepto de pruebas de software se remonta a los anos 1950, pero el desarrollo dirigido por pruebas (TDD) moderno fue sistematizado por Kent Beck a finales de los anos 1990.
“Las pruebas no miden la calidad. Las pruebas construyen la calidad” — Kent Beck
Por que escribir pruebas
- Prevencion de regresiones: Deteccion temprana de errores en funcionalidades existentes
- Documentacion: Las pruebas muestran como usar el codigo
- Confianza en la refactorizacion: Con pruebas, puedes mejorar sin miedo
- Mejora del diseno: El codigo testeable tiende a tener buen diseno
Piramide de pruebas
La “Piramide de pruebas” propuesta por Martin Fowler es un modelo que muestra los tipos de pruebas y su equilibrio:
flowchart TB
subgraph Pyramid["Piramide de pruebas"]
direction TB
E2E["Pruebas E2E (pocas)<br/>Automatizacion del navegador"]
Integration["Pruebas de integracion (moderadas)<br/>Pruebas de API, pruebas de componentes"]
Unit["Pruebas unitarias (muchas)<br/>Funciones, clases"]
end
E2E --> Integration --> Unit
| Tipo | Velocidad | Costo de mantenimiento | Fiabilidad | Cobertura |
|---|---|---|---|---|
| E2E | Lenta | Alto | Fragil | Amplia |
| Integracion | Moderada | Moderado | Moderada | Moderada |
| Unitaria | Rapida | Bajo | Estable | Limitada |
Referencia: Martin Fowler - Test Pyramid
Posicion de las pruebas de API
Las pruebas de API se clasifican como “pruebas de integracion” y tienen los siguientes beneficios:
- No se ven afectadas por cambios en la UI
- Mas rapidas y estables que E2E
- Prueban solicitudes HTTP reales
- Permiten probar el backend sin frontend
Step 1: Configuracion del proyecto
Primero, crearemos una aplicacion Express simple para probar.
Creacion del proyecto
mkdir api-testing-tutorial
cd api-testing-tutorial
npm init -y
npm install express
npm install -D jest supertest @types/jest @types/supertest typescript ts-jest
package.json (seccion scripts)
{
"scripts": {
"start": "node dist/index.js",
"build": "tsc",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
Configuracion de Jest
jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.test.ts'
],
// Configuracion antes y despues de las pruebas
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
// Umbral de cobertura (opcional)
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
Documentacion oficial: Jest Configuration
Step 2: Crear la API a probar
src/app.ts
import express, { Express, Request, Response, NextFunction } from 'express';
const app: Express = express();
app.use(express.json());
// Almacen de datos en memoria
interface User {
id: number;
name: string;
email: string;
}
let users: User[] = [
{ id: 1, name: 'Tanaka Taro', email: 'tanaka@example.com' },
{ id: 2, name: 'Sato Hanako', email: 'sato@example.com' }
];
// Middleware de manejo de errores
const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal Server Error' });
};
// GET /api/users - Lista de usuarios
app.get('/api/users', (req: Request, res: Response) => {
res.json(users);
});
// GET /api/users/:id - Detalle de usuario
app.get('/api/users/:id', (req: Request, res: Response) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'Usuario no encontrado' });
}
res.json(user);
});
// POST /api/users - Crear usuario
app.post('/api/users', (req: Request, res: Response) => {
const { name, email } = req.body;
// Validacion
if (!name || !email) {
return res.status(400).json({ error: 'El nombre y el correo electronico son obligatorios' });
}
if (!email.includes('@')) {
return res.status(400).json({ error: 'El formato del correo electronico no es valido' });
}
const newUser: User = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
// PUT /api/users/:id - Actualizar usuario
app.put('/api/users/:id', (req: Request, res: Response) => {
const userId = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
return res.status(404).json({ error: 'Usuario no encontrado' });
}
const { name, email } = req.body;
users[userIndex] = { ...users[userIndex], name, email };
res.json(users[userIndex]);
});
// DELETE /api/users/:id - Eliminar usuario
app.delete('/api/users/:id', (req: Request, res: Response) => {
const userId = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
return res.status(404).json({ error: 'Usuario no encontrado' });
}
users.splice(userIndex, 1);
res.status(204).send();
});
app.use(errorHandler);
// Funcion para resetear datos en pruebas
export const resetUsers = () => {
users = [
{ id: 1, name: 'Tanaka Taro', email: 'tanaka@example.com' },
{ id: 2, name: 'Sato Hanako', email: 'sato@example.com' }
];
};
export default app;
Step 3: Pruebas basicas de GET
src/app.test.ts
import request from 'supertest';
import app, { resetUsers } from './app';
describe('Users API', () => {
// Resetear datos antes de cada prueba
beforeEach(() => {
resetUsers();
});
// Pruebas de GET /api/users
describe('GET /api/users', () => {
it('should return all users', async () => {
const response = await request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toBeInstanceOf(Array);
expect(response.body.length).toBeGreaterThan(0);
});
it('should return users with correct properties', async () => {
const response = await request(app)
.get('/api/users')
.expect(200);
const user = response.body[0];
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('email');
});
});
// Pruebas de GET /api/users/:id
describe('GET /api/users/:id', () => {
it('should return a user by id', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body.id).toBe(1);
expect(response.body).toHaveProperty('name');
});
it('should return 404 for non-existent user', async () => {
const response = await request(app)
.get('/api/users/999')
.expect(404);
expect(response.body.error).toBe('Usuario no encontrado');
});
it('should handle invalid id format', async () => {
const response = await request(app)
.get('/api/users/invalid')
.expect(404);
});
});
});
Ejecutar pruebas
npm test
# Ejemplo de salida
# PASS src/app.test.ts
# Users API
# GET /api/users
# ✓ should return all users (25 ms)
# ✓ should return users with correct properties (8 ms)
# GET /api/users/:id
# ✓ should return a user by id (5 ms)
# ✓ should return 404 for non-existent user (4 ms)
Step 4: Pruebas de POST
Agregamos pruebas para la creacion de datos.
describe('POST /api/users', () => {
it('should create a new user', async () => {
const newUser = {
name: 'Yamada Jiro',
email: 'yamada@example.com'
};
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body).toMatchObject(newUser);
expect(response.body).toHaveProperty('id');
});
it('should return 400 when name is missing', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com' })
.expect(400);
expect(response.body.error).toContain('obligatorios');
});
it('should return 400 when email is missing', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Usuario de prueba' })
.expect(400);
expect(response.body.error).toContain('obligatorios');
});
it('should return 400 for invalid email format', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Usuario de prueba', email: 'invalid-email' })
.expect(400);
expect(response.body.error).toContain('formato');
});
});
Step 5: Pruebas de PUT/DELETE
describe('PUT /api/users/:id', () => {
it('should update an existing user', async () => {
const updatedData = {
name: 'Tanaka Taro (actualizado)',
email: 'tanaka-updated@example.com'
};
const response = await request(app)
.put('/api/users/1')
.send(updatedData)
.expect(200);
expect(response.body.name).toBe(updatedData.name);
expect(response.body.email).toBe(updatedData.email);
});
it('should return 404 for non-existent user', async () => {
await request(app)
.put('/api/users/999')
.send({ name: 'test', email: 'test@example.com' })
.expect(404);
});
});
describe('DELETE /api/users/:id', () => {
it('should delete an existing user', async () => {
await request(app)
.delete('/api/users/1')
.expect(204);
// Confirmar que fue eliminado
await request(app)
.get('/api/users/1')
.expect(404);
});
it('should return 404 for non-existent user', async () => {
await request(app)
.delete('/api/users/999')
.expect(404);
});
});
Step 6: Patrones de prueba y mejores practicas
Patron AAA (Arrange-Act-Assert)
Estructura las pruebas en tres fases:
it('should create a new user', async () => {
// Arrange: Preparar datos de prueba
const newUser = {
name: 'Yamada Jiro',
email: 'yamada@example.com'
};
// Act: Ejecutar lo que se va a probar
const response = await request(app)
.post('/api/users')
.send(newUser);
// Assert: Verificar resultados
expect(response.status).toBe(201);
expect(response.body).toMatchObject(newUser);
});
Mantener la independencia de las pruebas
describe('Users API', () => {
// Resetear datos antes de cada prueba
beforeEach(() => {
resetUsers();
});
// Limpiar despues de cada prueba (si es necesario)
afterEach(() => {
// Reset de mocks, etc.
jest.clearAllMocks();
});
// Limpiar despues de todas las pruebas
afterAll(async () => {
// Cerrar conexion a DB, etc.
});
});
Nombres de prueba descriptivos
// Buenos ejemplos: especificos e intencion clara
it('should return 404 when user does not exist', () => {});
it('should create user and return 201 status', () => {});
it('should validate email format and reject invalid emails', () => {});
// Malos ejemplos: intencion no clara
it('test1', () => {});
it('works', () => {});
it('success', () => {});
Convencion de nombres: El formato “should + comportamiento esperado” es comun.
Pruebas de valores limite
describe('Validation', () => {
it('should accept name with 1 character (minimum)', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'A', email: 'test@example.com' })
.expect(201);
});
it('should accept name with 100 characters (maximum)', async () => {
const longName = 'A'.repeat(100);
const response = await request(app)
.post('/api/users')
.send({ name: longName, email: 'test@example.com' })
.expect(201);
});
it('should reject empty name', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: '', email: 'test@example.com' })
.expect(400);
});
});
Step 7: Pruebas de API con autenticacion
Ejemplo de pruebas de API con autenticacion JWT:
describe('Protected API', () => {
const validToken = 'Bearer valid-jwt-token';
const invalidToken = 'Bearer invalid-token';
it('should return 401 without authorization header', async () => {
await request(app)
.get('/api/protected/resource')
.expect(401);
});
it('should return 401 with invalid token', async () => {
await request(app)
.get('/api/protected/resource')
.set('Authorization', invalidToken)
.expect(401);
});
it('should return 200 with valid token', async () => {
await request(app)
.get('/api/protected/resource')
.set('Authorization', validToken)
.expect(200);
});
});
Step 8: Uso de mocks
Mockeamos dependencias externas (base de datos, APIs externas).
Mock de base de datos
// src/services/userService.ts
import { db } from '../db';
export const userService = {
findAll: async () => db.users.findMany(),
findById: async (id: number) => db.users.findUnique({ where: { id } }),
create: async (data: { name: string; email: string }) => db.users.create({ data })
};
// src/services/userService.test.ts
import { userService } from './userService';
import { db } from '../db';
// Mock del modulo db
jest.mock('../db', () => ({
db: {
users: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn()
}
}
}));
describe('UserService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return all users', async () => {
const mockUsers = [
{ id: 1, name: 'User 1', email: 'user1@example.com' }
];
(db.users.findMany as jest.Mock).mockResolvedValue(mockUsers);
const result = await userService.findAll();
expect(db.users.findMany).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockUsers);
});
});
Mock de API externa
import axios from 'axios';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('External API integration', () => {
it('should fetch data from external API', async () => {
const mockData = { data: { id: 1, title: 'Test' } };
mockedAxios.get.mockResolvedValue(mockData);
const result = await fetchExternalData();
expect(mockedAxios.get).toHaveBeenCalledWith('https://api.example.com/data');
expect(result).toEqual(mockData.data);
});
it('should handle API errors', async () => {
mockedAxios.get.mockRejectedValue(new Error('Network Error'));
await expect(fetchExternalData()).rejects.toThrow('Network Error');
});
});
Cobertura de pruebas
Generar reporte de cobertura
npm test -- --coverage
---------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
---------------------|---------|----------|---------|---------|
All files | 95.24 | 88.89 | 100 | 95.24 |
app.ts | 95.24 | 88.89 | 100 | 95.24 |
---------------------|---------|----------|---------|---------|
Tipos de cobertura
| Tipo | Descripcion |
|---|---|
| Statements | Tasa de ejecucion de sentencias |
| Branches | Tasa de cobertura de ramas condicionales |
| Functions | Tasa de llamadas a funciones |
| Lines | Tasa de ejecucion de lineas |
Nota: El objetivo no es 100% de cobertura. Prioriza probar la logica de negocio importante.
Errores comunes y antipatrones
1. Dependencia entre pruebas
// Mal ejemplo: Depende del orden de las pruebas
it('should create user', async () => {
await request(app).post('/api/users').send({ name: 'Test', email: 'test@example.com' });
});
it('should have 3 users', async () => {
// Asume que la prueba anterior se ejecuto
const response = await request(app).get('/api/users');
expect(response.body.length).toBe(3);
});
// Buen ejemplo: Cada prueba es independiente
beforeEach(() => {
resetUsers(); // Resetear datos
});
it('should create user', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Test', email: 'test@example.com' });
expect(response.status).toBe(201);
});
2. Probar detalles de implementacion
// Mal ejemplo: Probar implementacion interna
it('should call database with correct SQL', async () => {
await userService.findById(1);
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users WHERE id = 1');
});
// Buen ejemplo: Probar comportamiento
it('should return user by id', async () => {
const user = await userService.findById(1);
expect(user.id).toBe(1);
});
3. Exceso de mocks
// Mal ejemplo: Mockear todo (la prueba no tiene sentido)
jest.mock('./userService');
jest.mock('./database');
jest.mock('./validator');
// Buen ejemplo: Solo mockear dependencias externas
jest.mock('./externalApiClient');
Resumen
Escribir pruebas de API proporciona los siguientes beneficios:
- Prevenir regresiones (ruptura de funcionalidades existentes)
- Documentar la especificacion de la API en codigo
- Confianza al refactorizar
- Verificacion automatica en pipelines CI/CD
Comienza con pruebas simples de GET/POST y expande gradualmente la cobertura.
Enlaces de referencia
Documentacion oficial
- Documentacion oficial de Jest - Framework de pruebas
- Supertest GitHub - Biblioteca de aserciones HTTP
- Testing Library - Mejores practicas de pruebas
Mejores practicas y articulos
- Martin Fowler - Test Pyramid - Explicacion de la piramide de pruebas
- Google Testing Blog - Blog de Google sobre pruebas
- Kent C. Dodds - Testing JavaScript - Guia completa de pruebas en JavaScript
Libros
- “Test-Driven Development” (Kent Beck) - El origen del TDD
- “Refactoring” (Martin Fowler) - Relacion entre pruebas y refactorizacion
Herramientas
- Jest - Framework de pruebas JavaScript
- Vitest - Framework de pruebas rapido para Vite
- Postman - Herramienta de desarrollo y prueba de APIs
- Insomnia - Cliente REST/GraphQL