Escribamos pruebas para REST API

Principiante | 90 min de lectura | 2025.12.20

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

  1. Prevencion de regresiones: Deteccion temprana de errores en funcionalidades existentes
  2. Documentacion: Las pruebas muestran como usar el codigo
  3. Confianza en la refactorizacion: Con pruebas, puedes mejorar sin miedo
  4. 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
TipoVelocidadCosto de mantenimientoFiabilidadCobertura
E2ELentaAltoFragilAmplia
IntegracionModeradaModeradoModeradaModerada
UnitariaRapidaBajoEstableLimitada

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

TipoDescripcion
StatementsTasa de ejecucion de sentencias
BranchesTasa de cobertura de ramas condicionales
FunctionsTasa de llamadas a funciones
LinesTasa 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

Mejores practicas y articulos

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
← Volver a la lista