Saltar al contenido principal

Guía de Testing en NestJS

El testing automatizado es uno de los pilares fundamentales del desarrollo de software moderno. En NestJS, la arquitectura basada en Inyección de Dependencias (DI) permite aislar y verificar componentes de forma predecible y reproducible.

Para seguir los ejemplos prácticos de esta guía, puedes basarte en el repositorio del curso: Kelocoes/compunet3-20252 en la rama main o nest/intro.


1. Fundamentos y Tipos de Pruebas

En el ecosistema backend con NestJS existen dos niveles principales de pruebas:

  1. Pruebas Unitarias (Unit Tests): Verifican funciones, métodos o clases en aislamiento estricto (como un UsersService o UsersController). Todas sus dependencias externas (bases de datos, servicios HTTP, repositorios TypeORM) se reemplazan por simulaciones (mocks o stubs).
  2. Pruebas de Integración y End-to-End (E2E Tests): Evalúan la interacción coordinada entre múltiples capas de la aplicación (enrutador, interceptores, guards, servicios y base de datos de pruebas) emulando peticiones HTTP reales mediante herramientas como Supertest.

2. Comandos de Ejecución con Jest en NestJS

NestJS integra Jest como ejecutor de pruebas estándar a través de los scripts preconfigurados en package.json:

Terminal: Comandos principales de pruebas
# 1. Ejecutar toda la suite de pruebas unitarias una sola vez
npm run test

# 2. Modo observador (Watch Mode): re-ejecuta pruebas al detectar cambios en archivos
npm run test:watch

# 3. Ejecutar un archivo o patrón específico de pruebas
npm test users.service.spec.ts

# 4. Modo depuración con Node inspector
npm run test:debug

# 5. Ejecutar suite de pruebas de integración End-to-End
npm run test:e2e

# 6. Generar el reporte de cobertura de código
npm run test:cov

3. Configuración y Métricas de Cobertura de Código

La cobertura de código (code coverage) mide la proporción del código fuente que ha sido ejecutada durante las pruebas automatizadas.

Métricas de Cobertura Reportadas por Jest

Al ejecutar npm run test:cov, Jest genera una tabla con cuatro indicadores clave:

  • % Stmts (Statements): Porcentaje de declaraciones o instrucciones ejecutadas.
  • % Branch (Branches): Porcentaje de ramas lógicas evaluadas (bifurcaciones if, else, operadores ternarios ? :, sentencias switch).
  • % Funcs (Functions): Porcentaje de funciones y métodos llamados durante la ejecución.
  • % Lines: Porcentaje de líneas físicas de código visitadas.
  • Uncovered Line #s: Indica exactamente qué líneas o rangos numéricos de código no fueron transitados por ningún caso de prueba.

Configuración de Jest y Umbrales Mínimos (coverageThreshold)

La configuración de Jest se define habitualmente en la sección "jest" de package.json o en un archivo independiente jest.config.js.

Para garantizar un estándar de calidad e impedir que se integren cambios sin pruebas suficientes, se pueden fijar umbrales mínimos (thresholds):

package.json
{
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s",
"!**/*.module.ts",
"!main.ts",
"!**/*.dto.ts",
"!**/*.entity.ts"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node",
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 85,
"lines": 85,
"statements": 85
}
}
}
}
¿Qué archivos excluir de la cobertura?

Archivos de configuración, módulos (*.module.ts), DTOs simples sin lógica, entidades de TypeORM y el archivo de arranque main.ts suelen excluirse mediante patrones de negación (!**/*.entity.ts) en collectCoverageFrom, ya que no contienen ramas lógicas de negocio y sesgan artificialmente las métricas.


4. Guía Práctica: Pruebas Unitarias de un Servicio

En esta sección implementaremos las pruebas unitarias para UsersService, simulando el repositorio de TypeORM y los servicios dependientes con Test.createTestingModule.

1

Configurar el Entorno de Prueba y Mocks

Creamos el archivo src/users/users.service.spec.ts. Definimos objetos de simulación (mock objects) con métodos Jest (jest.fn()) para emular el comportamiento de la base de datos sin conectarse a un motor real.

src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { RolesService } from '../auth/services/roles.service';
import { User } from './entities/user.entity';

// Objeto mock que simula los métodos del repositorio TypeORM
const mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};

// Objeto mock para servicios auxiliares
const mockRolesService = {
findByName: jest.fn(),
};

describe('UsersService', () => {
let service: UsersService;

beforeEach(async () => {
// Restablece el historial de llamadas de los mocks entre cada prueba
jest.clearAllMocks();

const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: RolesService,
useValue: mockRolesService,
},
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
}).compile();

service = module.get<UsersService>(UsersService);
});

it('debe estar definido e instanciado correctamente', () => {
expect(service).toBeDefined();
});
});
2

Probar la Creación de un Usuario (Caso Exitoso)

Validamos que cuando el rol existe, el servicio cree la entidad con el rol asociado y la guarde en el repositorio.

src/users/users.service.spec.ts
it('debe crear un usuario exitosamente cuando el rol existe', async () => {
const createUserDto = {
username: 'kelocoes',
email: 'kelocoes@icesi.edu.co',
passwordHash: 'secret_hash_123',
bio: 'Docente de Entornos Digitales',
roleName: 'admin',
};

const mockRole = { id: 1, name: 'admin', description: 'Administrador' };
const mockCreatedUser = { id: 10, ...createUserDto, role: mockRole };

// Configuramos los retornos esperados en los mocks
mockRolesService.findByName.mockResolvedValue(mockRole);
mockRepository.create.mockReturnValue(mockCreatedUser);
mockRepository.save.mockResolvedValue(mockCreatedUser);

const result = await service.create(createUserDto);

// Asertos de interacción
expect(mockRolesService.findByName).toHaveBeenCalledWith('admin');
expect(mockRepository.create).toHaveBeenCalledWith({
...createUserDto,
role: mockRole,
});
expect(mockRepository.save).toHaveBeenCalledWith(mockCreatedUser);
expect(result).toEqual(mockCreatedUser);
});
3

Probar el Manejo de Excepciones (Caso de Falla)

Verificamos que si el rol no se encuentra en el sistema, el servicio no intente guardar el usuario y lance RoleNotFoundException.

src/users/users.service.spec.ts
it('debe lanzar RoleNotFoundException si el rol no existe', async () => {
const createUserDto = {
username: 'usuario_invalido',
email: 'user@test.com',
passwordHash: 'pass123',
bio: '',
roleName: 'rol_inexistente',
};

// Simulamos que el rol no existe retornando null
mockRolesService.findByName.mockResolvedValue(null);

// Verificamos que la promesa sea rechazada con la excepción esperada
await expect(service.create(createUserDto)).rejects.toThrow();
expect(mockRepository.save).not.toHaveBeenCalled();
});
4

Probar Métodos de Consulta y Búsqueda

Evaluamos que findAll() retorne el arreglo esperado y que findOne() devuelva la entidad o lance UserNotFoundException.

src/users/users.service.spec.ts
it('debe retornar todos los usuarios registrados', async () => {
const mockUsers = [
{ id: 1, username: 'user1', email: 'user1@icesi.edu.co' },
{ id: 2, username: 'user2', email: 'user2@icesi.edu.co' },
];

mockRepository.find.mockResolvedValue(mockUsers);

const result = await service.findAll();

expect(mockRepository.find).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockUsers);
});

it('debe lanzar UserNotFoundException si el usuario no existe al buscar por id', async () => {
mockRepository.findOne.mockResolvedValue(null);

await expect(service.findOne(999)).rejects.toThrow();
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: 999 } });
});

5. Pruebas de Integración End-to-End (E2E) con Supertest

Las pruebas E2E levantan una instancia completa de la aplicación NestJS en memoria y prueban las rutas HTTP públicas como si se tratara de un cliente externo.

test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('UsersController (e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

afterAll(async () => {
await app.close();
});

it('GET /users debe retornar status 200 y un array', async () => {
const response = await request(app.getHttpServer())
.get('/users')
.expect(200);

expect(Array.isArray(response.body)).toBe(true);
});

it('GET /users/:id debe responder 404 cuando el id no existe', async () => {
await request(app.getHttpServer())
.get('/users/999999')
.expect(404);
});
});

Cuestionario de Autoevaluación

Cargando cuestionario...