JavaScript
Tutorial JavaScript: Introducción a pruebas en JS
Vitest y Jest: descubre los beneficios de las pruebas en desarrollo software y cómo implementarlas efectivamente con frameworks de testing.
Aprende JavaScript y certifícate¿Por qué hacer pruebas?: Conceptos básicos
Las pruebas de software son una parte fundamental del desarrollo profesional que a menudo se subestima, especialmente cuando estamos aprendiendo a programar. Muchos desarrolladores comienzan verificando su código manualmente: ejecutan la aplicación, prueban algunas funcionalidades y asumen que todo funciona correctamente. Sin embargo, este enfoque tiene serias limitaciones cuando los proyectos crecen en complejidad.
Beneficios de las pruebas automatizadas
- 1. Detección temprana de errores: Las pruebas permiten identificar problemas antes de que lleguen a producción, cuando son más costosos de corregir.
// Sin pruebas: descubrimos tarde que esto falla con ciertos inputs
function calculateDiscount(price, percentage) {
return price * percentage; // Error: no divide entre 100
}
// Con pruebas: detectamos el problema inmediatamente
test('applies 20% discount correctly', () => {
expect(calculateDiscount(100, 0.2)).toBe(20);
});
- 2. Documentación viva: Las pruebas actúan como una forma de documentación que siempre está actualizada y muestra exactamente cómo debe comportarse el código.
// Esta prueba documenta claramente el comportamiento esperado
test('should capitalize first letter of each word', () => {
expect(capitalizeWords('hello world')).toBe('Hello World');
});
- 3. Refactorización segura: Permite mejorar el código existente con la confianza de que no estamos rompiendo funcionalidades.
// Podemos refactorizar esta función...
function sumArray(numbers) {
let total = 0;
for (let i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}
// ...a una versión más moderna y concisa
function sumArray(numbers) {
return numbers.reduce((sum, num) => sum + num, 0);
}
// Y nuestras pruebas nos aseguran que sigue funcionando igual
- 4. Diseño mejorado: Escribir pruebas primero (TDD) nos obliga a pensar en la interfaz y comportamiento de nuestro código antes de implementarlo.
Tipos de pruebas
Existen diferentes niveles de pruebas, cada uno con propósitos específicos:
Pruebas unitarias: Verifican el funcionamiento correcto de unidades individuales de código (generalmente funciones o métodos) de forma aislada.
Pruebas de integración: Comprueban que diferentes partes del sistema funcionan correctamente juntas.
Pruebas end-to-end (E2E): Simulan el comportamiento de un usuario real interactuando con la aplicación completa.
El costo de no hacer pruebas
No implementar pruebas puede parecer que ahorra tiempo inicialmente, pero a largo plazo genera:
- Deuda técnica: Cada vez es más difícil modificar el código sin introducir regresiones.
- Ciclos de desarrollo más lentos: Pasar más tiempo depurando problemas que escribiendo nuevas funcionalidades.
- Menor confianza: Incertidumbre sobre si los cambios romperán algo en producción.
Enfoque pragmático
No es necesario alcanzar el 100% de cobertura de pruebas. Un enfoque pragmático consiste en:
// Priorizar pruebas para lógica de negocio crítica
function calculateMortgage(principal, rate, years) {
const monthlyRate = rate / 100 / 12;
const payments = years * 12;
return (
(principal * monthlyRate * Math.pow(1 + monthlyRate, payments)) /
(Math.pow(1 + monthlyRate, payments) - 1)
);
}
// Esta función merece buenas pruebas con diferentes escenarios
test('calculates mortgage correctly for standard case', () => {
expect(calculateMortgage(200000, 3.5, 30)).toBeCloseTo(898.09);
});
Principios para pruebas efectivas
FIRST: Las buenas pruebas son Fast (rápidas), Independent (independientes), Repeatable (repetibles), Self-validating (auto-validables) y Thorough (completas).
Arrange-Act-Assert: Estructura clara para organizar las pruebas:
test('should filter out negative numbers', () => {
// Arrange (preparar)
const numbers = [5, -3, 10, -7, 0];
// Act (actuar)
const result = filterPositive(numbers);
// Assert (verificar)
expect(result).toEqual([5, 10, 0]);
});
- Prueba comportamientos, no implementaciones: Enfócate en lo que el código debe hacer, no en cómo lo hace.
Integración en el flujo de trabajo
Las pruebas no son una actividad aislada, sino parte integral del desarrollo:
- Integración continua: Ejecutar pruebas automáticamente con cada commit o pull request.
- TDD (Test-Driven Development): Escribir la prueba antes que el código de producción.
- Pruebas de regresión: Asegurar que los errores corregidos no vuelvan a aparecer.
Adoptar una cultura de pruebas desde el principio de un proyecto establece bases sólidas para su crecimiento y mantenimiento a largo plazo, reduciendo significativamente el estrés y los problemas en fases avanzadas del desarrollo.
Herramientas principales: Vitest/Jest fundamentales
En el ecosistema de JavaScript, contar con frameworks de pruebas robustos es esencial para implementar una estrategia de testing efectiva. Dos de las herramientas más destacadas son Jest y Vitest, que proporcionan todo lo necesario para escribir y ejecutar pruebas de manera eficiente.
Jest: El estándar de facto
Jest es un framework de pruebas desarrollado por Facebook que se ha convertido en la opción predeterminada para muchos proyectos JavaScript. Su popularidad se debe a su enfoque "batteries-included" (todo incluido):
// Instalación de Jest
// npm install --save-dev jest
Las características principales que ofrece Jest incluyen:
- Configuración mínima: Funciona inmediatamente sin necesidad de configuración adicional.
- Ejecución en paralelo: Ejecuta pruebas de forma simultánea para mayor velocidad.
- Cobertura de código integrada: Genera informes detallados sobre qué partes del código están siendo probadas.
- Mocking incorporado: Facilita la simulación de módulos, funciones y dependencias.
Vitest: La alternativa moderna
Vitest es un framework más reciente diseñado específicamente para proyectos que utilizan Vite como bundler, ofreciendo una experiencia de desarrollo más rápida:
// Instalación de Vitest
// npm install --save-dev vitest
Sus ventajas incluyen:
- Rendimiento superior: Aprovecha la arquitectura de Vite para ofrecer tiempos de ejecución extremadamente rápidos.
- Compatibilidad con Jest: API similar que facilita la migración desde Jest.
- Soporte nativo para TypeScript y ESM (ECMAScript Modules).
- Hot Module Replacement (HMR) para pruebas, mejorando el ciclo de desarrollo.
Estructura básica de una prueba
Tanto Jest como Vitest comparten una sintaxis similar para escribir pruebas:
// math.js
export function sum(a, b) {
return a + b;
}
// math.test.js
import { expect, test } from 'vitest'; // o 'jest'
import { sum } from './math';
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
Funciones esenciales
Ambos frameworks proporcionan un conjunto de funciones fundamentales para organizar y estructurar pruebas:
- test() (o it()): Define un caso de prueba individual.
- describe(): Agrupa pruebas relacionadas en bloques lógicos.
- beforeEach() y afterEach(): Ejecutan código antes y después de cada prueba.
- beforeAll() y afterAll(): Ejecutan código una vez antes y después de todas las pruebas.
import { describe, test, beforeEach, expect } from 'vitest';
describe('User authentication', () => {
let user;
beforeEach(() => {
user = { name: 'John', isLoggedIn: false };
});
test('should log in successfully', () => {
user.isLoggedIn = true;
expect(user.isLoggedIn).toBe(true);
});
test('should have correct username', () => {
expect(user.name).toBe('John');
});
});
Matchers: Verificación de resultados
Los matchers son funciones que permiten verificar valores de diferentes maneras. Algunos de los más utilizados son:
- toBe(): Compara valores primitivos o referencias de objetos.
- toEqual(): Compara recursivamente todas las propiedades de objetos.
- toContain(): Verifica si un array contiene un elemento específico.
- toThrow(): Comprueba si una función lanza una excepción.
test('demonstration of common matchers', () => {
// Igualdad exacta
expect(2 + 2).toBe(4);
// Comparación profunda de objetos
expect({ name: 'user', age: 30 }).toEqual({ name: 'user', age: 30 });
// Comprobación de arrays
expect(['apple', 'banana']).toContain('apple');
// Verificación de excepciones
const errorFn = () => { throw new Error('Something went wrong'); };
expect(errorFn).toThrow();
});
Mocks: Simulación de dependencias
El mocking es una técnica crucial para aislar el código que estamos probando:
// userService.js
import api from './api';
export async function getUser(id) {
const response = await api.fetchUser(id);
return response.data;
}
// userService.test.js
import { vi, test, expect } from 'vitest';
import { getUser } from './userService';
import api from './api';
// Mockear el módulo api
vi.mock('./api', () => ({
fetchUser: vi.fn()
}));
test('should fetch user data', async () => {
// Configurar el comportamiento del mock
api.fetchUser.mockResolvedValue({
data: { id: 1, name: 'Alice' }
});
const user = await getUser(1);
// Verificar que la función mock fue llamada correctamente
expect(api.fetchUser).toHaveBeenCalledWith(1);
expect(user).toEqual({ id: 1, name: 'Alice' });
});
Configuración básica
Tanto Jest como Vitest pueden configurarse a través de archivos específicos:
// vitest.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
coverage: {
reporter: ['text', 'html'],
exclude: ['node_modules/']
},
globals: true
}
});
Ejecución de pruebas
Para ejecutar las pruebas, se utilizan comandos en el terminal o scripts en el package.json
:
{
"scripts": {
"test": "vitest",
"test:coverage": "vitest run --coverage"
}
}
Integración con herramientas de CI/CD
Ambos frameworks se integran fácilmente con sistemas de integración continua como GitHub Actions, CircleCI o Jenkins:
# Ejemplo básico de GitHub Actions
name: Run Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm ci
- run: npm test
La elección entre Jest y Vitest dependerá principalmente del entorno de desarrollo que estés utilizando. Si trabajas con Vite, Vitest ofrece ventajas significativas de rendimiento, mientras que Jest sigue siendo una opción sólida y probada para cualquier proyecto JavaScript.
Terminología: pruebas, fixtures, aserciones
Para dominar el testing en JavaScript es esencial comprender la terminología específica que forma parte del vocabulario técnico en este ámbito. Familiarizarse con estos conceptos no solo facilita la comunicación entre desarrolladores, sino que también permite aprovechar mejor las herramientas de pruebas disponibles.
Tipos de pruebas
Aunque ya hemos mencionado brevemente los tipos de pruebas, es importante profundizar en sus definiciones precisas:
- Pruebas unitarias: Evalúan componentes individuales de código de forma aislada, generalmente una función o método específico.
// Función a probar
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
// Prueba unitaria
test('capitalizes the first letter of a string', () => {
expect(capitalize('hello')).toBe('Hello');
});
- Pruebas de integración: Verifican que varios componentes funcionan correctamente juntos, probando las interacciones entre ellos.
// Prueba de integración entre un servicio y un repositorio
test('user service creates user in database', async () => {
const user = { username: 'testuser', email: 'test@example.com' };
await userService.create(user);
const savedUser = await userRepository.findByUsername('testuser');
expect(savedUser.email).toBe('test@example.com');
});
Pruebas de componentes: En aplicaciones frontend, evalúan el comportamiento de componentes UI aislados.
Pruebas E2E (End-to-End): Simulan el comportamiento de un usuario real interactuando con la aplicación completa.
Aserciones (Assertions)
Las aserciones son declaraciones que verifican si un valor cumple con ciertas condiciones. Son el núcleo de cualquier prueba, ya que definen qué consideramos un comportamiento correcto.
// Ejemplos de aserciones comunes
test('demonstration of assertion types', () => {
// Igualdad
expect(2 + 2).toBe(4); // Igualdad estricta (===)
expect({ name: 'test' }).toEqual({ name: 'test' }); // Igualdad profunda
// Verdad/Falsedad
expect(true).toBeTruthy(); // Evalúa a true
expect(0).toBeFalsy(); // Evalúa a false
expect(null).toBeNull(); // Es exactamente null
expect(undefined).toBeUndefined(); // Es exactamente undefined
// Números
expect(10).toBeGreaterThan(5); // Mayor que
expect(5).toBeLessThanOrEqual(5); // Menor o igual que
expect(0.1 + 0.2).toBeCloseTo(0.3); // Aproximadamente igual (para decimales)
// Strings
expect('hello world').toMatch(/world/); // Coincide con regex
expect('hello').toContain('ell'); // Contiene substring
// Arrays
expect([1, 2, 3]).toContain(2); // Contiene elemento
expect([1, 2, 3]).toHaveLength(3); // Tiene longitud específica
});
Fixtures
Los fixtures son datos o estados predefinidos que se utilizan para establecer un entorno consistente para las pruebas. Permiten:
- Reutilizar configuraciones comunes entre pruebas
- Mantener las pruebas deterministas
- Reducir la duplicación de código
// Ejemplo de fixture como función auxiliar
function createUserFixture(overrides = {}) {
return {
id: 1,
username: 'testuser',
email: 'test@example.com',
isActive: true,
createdAt: new Date('2023-01-01'),
...overrides
};
}
test('active users can login', () => {
const user = createUserFixture();
expect(authService.canLogin(user)).toBe(true);
});
test('inactive users cannot login', () => {
const inactiveUser = createUserFixture({ isActive: false });
expect(authService.canLogin(inactiveUser)).toBe(false);
});
Los fixtures pueden implementarse de varias formas:
- Objetos literales: Definidos directamente en los archivos de prueba
- Factory functions: Funciones que generan datos de prueba personalizables
- Archivos externos: JSON, CSV u otros formatos para datos más complejos
- Hooks de prueba: Usando
beforeEach
para configurar el estado
Mocks, Stubs y Spies
Estas herramientas permiten aislar el código que se está probando de sus dependencias:
- Mocks: Reemplazos completos de funciones o módulos con implementaciones controladas.
// Mock completo de un módulo
vi.mock('./database', () => ({
query: vi.fn().mockResolvedValue([{ id: 1, name: 'Test Item' }])
}));
test('fetches items from database', async () => {
const items = await itemService.getAll();
expect(items).toHaveLength(1);
expect(items[0].name).toBe('Test Item');
});
- Stubs: Implementaciones simplificadas que devuelven valores predefinidos.
// Stub de una función específica
const originalCalculateTotal = orderService.calculateTotal;
orderService.calculateTotal = () => 100;
test('applies discount to order total', () => {
const discountedTotal = orderService.applyDiscount(0.1);
expect(discountedTotal).toBe(90);
});
// Restaurar la implementación original
orderService.calculateTotal = originalCalculateTotal;
- Spies: Observan y registran llamadas a funciones sin modificar su comportamiento.
// Spy para verificar que una función fue llamada
const logSpy = vi.spyOn(console, 'log');
function greet(name) {
console.log(`Hello, ${name}!`);
return name;
}
test('greet logs message and returns name', () => {
const result = greet('Alice');
expect(logSpy).toHaveBeenCalledWith('Hello, Alice!');
expect(result).toBe('Alice');
});
Test Runners y Suites
Test Runner: El motor que ejecuta las pruebas y reporta los resultados. Vitest y Jest incluyen sus propios test runners.
Test Suite: Agrupación lógica de pruebas relacionadas, generalmente implementada con la función
describe
.
describe('Authentication module', () => {
describe('login function', () => {
test('succeeds with valid credentials', () => {
// Prueba aquí
});
test('fails with invalid password', () => {
// Prueba aquí
});
});
describe('logout function', () => {
test('clears user session', () => {
// Prueba aquí
});
});
});
Cobertura de código
La cobertura mide qué porcentaje del código está siendo ejecutado por las pruebas. Se suele analizar en términos de:
- Cobertura de líneas: Porcentaje de líneas de código ejecutadas
- Cobertura de ramas: Porcentaje de ramas condicionales (if/else) evaluadas
- Cobertura de funciones: Porcentaje de funciones llamadas
- Cobertura de declaraciones: Porcentaje de declaraciones ejecutadas
// Ejecutar pruebas con análisis de cobertura
// npx vitest run --coverage
Test-Driven Development (TDD)
TDD es una metodología que sigue un ciclo de tres pasos:
- Red: Escribir una prueba que falle
- Green: Implementar el código mínimo para que la prueba pase
- Refactor: Mejorar el código manteniendo las pruebas en verde
// 1. Red: Escribir prueba que falla
test('sum adds two numbers correctly', () => {
expect(sum(2, 3)).toBe(5);
});
// La prueba falla porque sum() no existe
// 2. Green: Implementar código mínimo
function sum(a, b) {
return a + b;
}
// La prueba pasa
// 3. Refactor: Mejorar implementación si es necesario
function sum(a, b) {
return Number(a) + Number(b); // Más robusta con conversión de tipos
}
// La prueba sigue pasando
Pruebas de snapshot
Las pruebas de snapshot capturan la salida de un componente o función y la comparan con una versión guardada anteriormente, útil para detectar cambios inesperados.
test('generates correct user summary', () => {
const user = createUserFixture();
const summary = generateUserSummary(user);
// Compara con snapshot guardado o crea uno nuevo
expect(summary).toMatchSnapshot();
});
Comprender esta terminología proporciona una base sólida para desarrollar una estrategia de pruebas efectiva en cualquier proyecto JavaScript, facilitando la comunicación con otros desarrolladores y el aprovechamiento de las capacidades completas de los frameworks de testing.
Ejercicios de esta lección Introducción a pruebas en JS
Evalúa tus conocimientos de esta lección Introducción a pruebas en JS con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Funciones flecha
Polimorfismo
Array
Transformación con map()
Gestor de tareas con JavaScript
Manipulación DOM
Funciones
Funciones flecha
Async / Await
Creación y uso de variables
Excepciones
Promises
Funciones cierre (closure)
Herencia
Herencia
Estructuras de control
Selección de elementos DOM
Modificación de elementos DOM
Filtrado con filter() y find()
Funciones cierre (closure)
Funciones
Mapas con Map
Reducción con reduce()
Callbacks
Manipulación DOM
Promises
Async / Await
Eventos del DOM
Async / Await
Promises
Filtrado con filter() y find()
Callbacks
Creación de clases y objetos Restaurante
Reducción con reduce()
Filtrado con filter() y find()
Reducción con reduce()
Conjuntos con Set
Herencia de clases
Eventos del DOM
Clases y objetos
Modificación de elementos DOM
Mapas con Map
Introducción a JavaScript
Funciones
Tipos de datos
Clases y objetos
Array
Conjuntos con Set
Array
Encapsulación
Clases y objetos
Uso de operadores
Uso de operadores
Estructuras de control
Excepciones
Transformación con map()
Funciones flecha
Selección de elementos DOM
Encapsulación
Mapas con Map
Creación y uso de variables
Polimorfismo
Tipos de datos
Estructuras de control
Todas las lecciones de JavaScript
Accede a todas las lecciones de JavaScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Javascript
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Funciones Cierre (Closure)
Sintaxis
Arrays Y Métodos
Estructuras De Datos
Conjuntos Con Set
Estructuras De Datos
Mapas Con Map
Estructuras De Datos
Funciones Flecha
Programación Funcional
Filtrado Con Filter() Y Find()
Programación Funcional
Transformación Con Map()
Programación Funcional
Reducción Con Reduce()
Programación Funcional
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Manipulación Dom
Dom
Selección De Elementos Dom
Dom
Modificación De Elementos Dom
Dom
Eventos Del Dom
Dom
Callbacks
Programación Asíncrona
Promises
Programación Asíncrona
Async / Await
Programación Asíncrona
Certificados de superación de JavaScript
Supera todos los ejercicios de programación del curso de JavaScript y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender los beneficios de las pruebas automatizadas
- Diferenciar tipos de pruebas: unitarias, de integración y E2E
- Implementar buenas prácticas y principios de pruebas efectivas
- Explorar herramientas principales: Vitest y Jest
- Aplicar un enfoque pragmático a la cobertura de pruebas