En esta página

Testing en NestJS — unitario y end-to-end

14 min lectura TextoCap. 5 — Producción

La filosofía del testing en NestJS

NestJS fue diseñado desde el principio para ser testeable. Su sistema de inyección de dependencias hace trivial reemplazar implementaciones reales con mocks en los tests. La combinación de @nestjs/testing con Jest crea una experiencia de testing de primera clase.

Existen tres niveles de testing en NestJS:

  1. Tests unitarios: Prueban un servicio o componente en aislamiento, con todas sus dependencias mockeadas
  2. Tests de integración: Prueban un módulo completo con algunos servicios reales y otros mockeados
  3. Tests end-to-end (e2e): Prueban la aplicación completa desde la perspectiva HTTP, incluyendo base de datos

Configuración de Jest

NestJS configura Jest automáticamente al crear el proyecto. El package.json incluye:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "jest": {
    "moduleFileExtensions": ["js", "json", "ts"],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": { "^.+\\.(t|j)s$": "ts-jest" },
    "collectCoverageFrom": ["**/*.(t|j)s"],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}

El TestingModule — La pieza central

Test.createTestingModule() crea un módulo NestJS especial para testing. Es como @Module() pero diseñado para ser construido dinámicamente en los tests:

import { Test, TestingModule } from '@nestjs/testing';

describe('UsuariosService', () => {
  let module: TestingModule;
  let service: UsuariosService;

  beforeEach(async () => {
    module = await Test.createTestingModule({
      providers: [
        UsuariosService,
        { provide: getRepositoryToken(Usuario), useValue: mockRepo },
        { provide: EmailService, useValue: mockEmailService },
      ],
    }).compile();

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

  afterEach(async () => {
    await module.close();
  });
});

Estrategias de mocking

Mock de repositorios de TypeORM

const mockRepo = {
  find: jest.fn(),
  findOne: jest.fn(),
  findOneOrFail: jest.fn(),
  save: jest.fn(),
  create: jest.fn(),
  update: jest.fn(),
  delete: jest.fn(),
  softDelete: jest.fn(),
  createQueryBuilder: jest.fn().mockReturnValue({
    where: jest.fn().mockReturnThis(),
    andWhere: jest.fn().mockReturnThis(),
    orderBy: jest.fn().mockReturnThis(),
    skip: jest.fn().mockReturnThis(),
    take: jest.fn().mockReturnThis(),
    getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
  }),
};

Mock de servicios con auto-mocking

const module = await Test.createTestingModule({
  providers: [ProductosService],
})
  .useMocker((token) => {
    // Auto-mockea automáticamente todas las dependencias
    if (typeof token === 'function') {
      const mockMetadata = moduleMocker.getMetadata(token);
      const Mock = moduleMocker.generateFromMetadata(mockMetadata);
      return new Mock();
    }
    return {};
  })
  .compile();

Testing del controlador

Para tests de controladores, puedes testear sin levantar el servidor HTTP:

// productos/productos.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { ProductosController } from './productos.controller';
import { ProductosService } from './productos.service';

describe('ProductosController', () => {
  let controller: ProductosController;
  let service: jest.Mocked<ProductosService>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [ProductosController],
      providers: [
        {
          provide: ProductosService,
          useValue: {
            findAll: jest.fn().mockResolvedValue({ datos: [], total: 0 }),
            findOne: jest.fn(),
            create: jest.fn(),
            update: jest.fn(),
            remove: jest.fn(),
          },
        },
      ],
    }).compile();

    controller = module.get<ProductosController>(ProductosController);
    service = module.get(ProductosService);
  });

  it('findAll debería llamar al servicio con los filtros correctos', async () => {
    const filtros = { pagina: 1, limite: 10 };
    await controller.findAll(filtros as FiltrosProductoDto);
    expect(service.findAll).toHaveBeenCalledWith(filtros);
  });
});

Tests de guards

// auth/guards/jwt-auth.guard.spec.ts
import { Test } from '@nestjs/testing';
import { JwtAuthGuard } from './jwt-auth.guard';
import { JwtService } from '@nestjs/jwt';
import { Reflector } from '@nestjs/core';
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';

describe('JwtAuthGuard', () => {
  let guard: JwtAuthGuard;
  let jwtService: jest.Mocked<JwtService>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        JwtAuthGuard,
        { provide: JwtService, useValue: { verifyAsync: jest.fn() } },
        { provide: Reflector, useValue: { getAllAndOverride: jest.fn() } },
      ],
    }).compile();

    guard = module.get<JwtAuthGuard>(JwtAuthGuard);
    jwtService = module.get(JwtService);
  });

  const crearContexto = (token?: string): ExecutionContext => ({
    switchToHttp: () => ({
      getRequest: () => ({
        headers: token ? { authorization: `Bearer ${token}` } : {},
      }),
    }),
    getHandler: jest.fn(),
    getClass: jest.fn(),
  } as unknown as ExecutionContext);

  it('debería lanzar UnauthorizedException sin token', async () => {
    const reflector = { getAllAndOverride: jest.fn().mockReturnValue(false) };
    const module2 = await Test.createTestingModule({
      providers: [
        JwtAuthGuard,
        { provide: JwtService, useValue: jwtService },
        { provide: Reflector, useValue: reflector },
      ],
    }).compile();

    const guard2 = module2.get<JwtAuthGuard>(JwtAuthGuard);
    await expect(guard2.canActivate(crearContexto()))
      .rejects
      .toThrow(UnauthorizedException);
  });
});

Cobertura de código

Para generar un reporte de cobertura:

npm run test:cov

El objetivo recomendado es 80% de cobertura como mínimo para código de producción. No obsesiones con llegar al 100% —los tests de más valor son los que cubren lógica de negocio compleja, casos límite y flujos de error.

Con una suite de tests robusta, puedes refactorizar y añadir features con confianza. En la lección final, pondrás todo en práctica construyendo una API REST completa de librería con todos los conceptos del curso.

Usa SQLite en memoria para tests e2e
Para los tests end-to-end, usa SQLite con `database: ':memory:'` en lugar de una base de datos PostgreSQL real. Los tests se ejecutan mucho más rápido y no requieren infraestructura externa. TypeORM es compatible con SQLite para la mayoría de funcionalidades. Para tests de features específicas de PostgreSQL (jsonb, arrays, etc.), sí necesitarás una BD real o usar Docker.
jest.fn() vs jest.spyOn() — cuándo usar cada uno
Usa `jest.fn()` cuando creas el mock completo del objeto (como en `useValue` del módulo de testing). Usa `jest.spyOn()` cuando quieres mockear solo un método de una instancia real, manteniendo los demás métodos intactos. `jest.spyOn()` es más adecuado para mocks parciales y se puede restaurar con `mockRestore()`.