Introducción: ¿Por qué probar código TypeScript?
TypeScript es una herramienta poderosa que añade tipado estático a JavaScript, permitiendo detectar errores en tiempo de compilación. Sin embargo, incluso el sistema de tipos más estricto no garantiza que tu código funcione correctamente en condiciones reales. Los errores lógicos, el manejo incorrecto de casos límite y los problemas de integración de módulos quedan fuera del alcance del compilador. Aquí es donde las pruebas vienen al rescate.
Las pruebas en TypeScript no son solo una verificación de funciones. Es un proceso integral que incluye tres niveles clave: Unit (pruebas unitarias), Integration (pruebas de integración) y E2E (pruebas de extremo a extremo). Cada uno resuelve su propio propósito y juntos forman la "pirámide de pruebas", garantizando la confiabilidad de la aplicación.
En este artículo, analizaremos cómo escribir pruebas para TypeScript en los tres niveles, utilizando herramientas modernas: Jest, Supertest y Playwright. Aprenderás qué errores ayudan a prevenir estas pruebas y cómo implementarlas en tu proyecto.
Pruebas unitarias: Verificación de módulos aislados
Las pruebas unitarias son la base de la pirámide. Prueban funciones, clases o métodos individuales de forma aislada de dependencias externas (bases de datos, API, sistema de archivos). El objetivo principal es asegurarse de que cada "bloque" de tu aplicación funcione correctamente.
Herramientas y configuración
El framework más popular para pruebas unitarias en TypeScript es Jest. Proporciona soporte integrado para TypeScript a través de ts-jest o @swc/jest.
Instalación de Jest en el proyecto:
npm install --save-dev jest @types/jest ts-jestnpx ts-jest config:initEjemplo de prueba unitaria
Consideremos una función simple para calcular un descuento:
// discount.tsexport function applyDiscount(price: number, discountPercent: number): number { if (discountPercent < 0 || discountPercent > 100) { throw new Error('El porcentaje de descuento debe estar entre 0 y 100'); } return price - (price * discountPercent) / 100;}Escribamos pruebas unitarias que cubran los escenarios principales:
// discount.test.tsimport { applyDiscount } from './discount';
describe('applyDiscount', () => { it('debe aplicar correctamente un descuento del 20%', () => { expect(applyDiscount(100, 20)).toBe(80); });
it('debe devolver el precio original con un descuento del 0%', () => { expect(applyDiscount(50, 0)).toBe(50); });
it('debe devolver 0 con un descuento del 100%', () => { expect(applyDiscount(200, 100)).toBe(0); });
it('debe lanzar un error con descuento negativo', () => { expect(() => applyDiscount(100, -10)).toThrow('El porcentaje de descuento debe estar entre 0 y 100'); });
it('debe lanzar un error con descuento mayor al 100%', () => { expect(() => applyDiscount(100, 150)).toThrow('El porcentaje de descuento debe estar entre 0 y 100'); });});Importante: Las pruebas unitarias no deben acceder a recursos externos. En su lugar, utiliza mocks para reemplazar dependencias, por ejemplo, con jest.mock().
Pruebas de integración: Interacción entre módulos
Las pruebas de integración verifican cómo diferentes partes del sistema funcionan juntas. Esto puede ser la interacción entre un servicio y una base de datos, un controlador y un repositorio, o entre microservicios. A diferencia de las pruebas unitarias, aquí no aislamos los componentes por completo.
Ejemplo: Pruebas de API REST
Supongamos que tenemos una aplicación Express con un endpoint. Para probar solicitudes HTTP, utilizamos Supertest.
// userController.tsimport { Router, Request, Response } from 'express';
interface User { id: number; name: string;}
const users: User[] = [{ id: 1, name: 'Alice' }];