Pruebas Unitarias
Aprende a escribir pruebas unitarias con Jest.
Instalación
bash
npm install -D jest ts-jest @types/jestnpx ts-jest config:initConfiguración
javascript
// jest.config.jsmodule.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], testMatch: ['**/*.spec.ts', '**/*.test.ts'], moduleNameMapper: { '@/(.*)': '<rootDir>/src/$1', }, collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', ], coverageDirectory: 'coverage', clearMocks: true,};Scripts
json
{ "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }}Estructura de Pruebas
src/
├── services/
│ ├── product.service.ts
│ └── __tests__/
│ └── product.service.spec.ts
├── utils/
│ ├── helpers.ts
│ └── __tests__/
│ └── helpers.spec.ts
Prueba Básica
typescript
// src/utils/__tests__/helpers.spec.tsimport { calculateDiscount, generateSlug } from '../helpers'; describe('Helpers', () => { describe('calculateDiscount', () => { it('debe calcular el descuento correctamente', () => { const result = calculateDiscount(100, 20); expect(result).toBe(80); }); it('debe retornar el precio original si el descuento es 0', () => { const result = calculateDiscount(100, 0); expect(result).toBe(100); }); it('debe lanzar error si el descuento es mayor a 100', () => { expect(() => calculateDiscount(100, 150)).toThrow(); }); }); describe('generateSlug', () => { it('debe convertir a minúsculas y reemplazar espacios', () => { expect(generateSlug('iPhone 15 Pro')).toBe('iphone-15-pro'); }); it('debe eliminar caracteres especiales', () => { expect(generateSlug('Cámara 4K!')).toBe('camara-4k'); }); });});Mocking con Jest
typescript
// src/services/__tests__/product.service.spec.tsimport { ProductService } from '../product.service';import { prisma } from '../../config/database';import { AppError } from '../../errors/AppError'; // Mock de Prismajest.mock('../../config/database', () => ({ prisma: { product: { findMany: jest.fn(), findUnique: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn(), count: jest.fn(), }, },})); describe('ProductService', () => { let service: ProductService; beforeEach(() => { service = new ProductService(); jest.clearAllMocks(); }); describe('findById', () => { it('debe retornar el producto si existe', async () => { const mockProduct = { id: '123', name: 'iPhone', price: 999 }; (prisma.product.findUnique as jest.Mock).mockResolvedValue(mockProduct); const result = await service.findById('123'); expect(result).toEqual(mockProduct); expect(prisma.product.findUnique).toHaveBeenCalledWith({ where: { id: '123' }, include: { category: true }, }); }); it('debe lanzar AppError si no existe', async () => { (prisma.product.findUnique as jest.Mock).mockResolvedValue(null); await expect(service.findById('999')).rejects.toThrow(AppError); await expect(service.findById('999')).rejects.toThrow('Producto no encontrado'); }); }); describe('create', () => { const createData = { name: 'MacBook', price: 1999, categoryId: 'cat-1' }; it('debe crear el producto con slug', async () => { (prisma.product.findUnique as jest.Mock).mockResolvedValue(null); (prisma.product.create as jest.Mock).mockResolvedValue({ id: '1', ...createData, slug: 'macbook', }); const result = await service.create(createData); expect(result.slug).toBe('macbook'); }); it('debe lanzar error si el producto ya existe', async () => { (prisma.product.findUnique as jest.Mock).mockResolvedValue({ id: '1' }); await expect(service.create(createData)).rejects.toThrow('ya existe'); }); });});Ejecutar Pruebas
bash
# Todas las pruebasnpm test # Con watch modenpm run test:watch # Con coberturanpm run test:coverageResumen
- ✅ Jest configurado con TypeScript
- ✅ Estructura de pruebas organizada
- ✅ Mocking de dependencias
- ✅ Pruebas de error handling
Próxima clase: Pruebas de Integración! 🚀