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, 20)).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 usando el método reduce que vimos en programación funcional
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 nos obliga a pensar en la interfaz y comportamiento de nuestro código antes de implementarlo, lo que normalmente resulta en un diseño más limpio y modular.
Conexión con conceptos previos
Las pruebas se complementan perfectamente con los conceptos que hemos visto anteriormente:
- Funciones puras (Programación Funcional): Las funciones puras que vimos en módulos anteriores son ideales para las pruebas unitarias, ya que:
- Siempre devuelven el mismo resultado para las mismas entradas
- No tienen efectos secundarios
- No dependen de estado externo
// Función pura del módulo de programación funcional
function filterPositive(numbers) {
return numbers.filter(num => num >= 0);
}
// Es muy fácil de probar
test('filters out negative numbers', () => {
expect(filterPositive([5, -3, 10, -7, 0])).toEqual([5, 10, 0]);
});
- Calidad de código (ESLint): Mientras que ESLint nos ayuda a mantener un código limpio y evitar errores sintácticos, las pruebas verifican que nuestro código funciona correctamente a nivel lógico. Son herramientas complementarias para asegurar la calidad.
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);
});
Estructura de una prueba básica
Una buena práctica es seguir el patrón "Arrange-Act-Assert" para organizar tus pruebas:
test('should filter out negative numbers', () => {
// Arrange (preparar): configuramos los datos de prueba
const numbers = [5, -3, 10, -7, 0];
// Act (actuar): ejecutamos la función que queremos probar
const result = filterPositive(numbers);
// Assert (verificar): comprobamos que el resultado es el esperado
expect(result).toEqual([5, 10, 0]);
});
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 y Vitest: ¿Qué son?
Jest es un framework de pruebas desarrollado por Facebook que se ha convertido en el estándar para muchos proyectos JavaScript.
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 básica
Para comenzar a usar estas herramientas en tu proyecto:
// Instalación de Jest
// npm install --save-dev jest
// Instalación de Vitest
// npm install --save-dev vitest
Nuestro ejemplo integrador: Utility functions
A lo largo de esta lección, trabajaremos con un pequeño módulo de funciones utilitarias que podrían venir de nuestras lecciones previas de programación funcional:
// utils.js
export function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export function calculateTax(amount, taxRate) {
return amount * taxRate / 100;
}
export function filterItems(items, predicate) {
return items.filter(predicate);
}
Estructura básica de una prueba
Tanto Jest como Vitest comparten una sintaxis similar para escribir pruebas:
// utils.test.js
import { expect, test } from 'vitest'; // o 'jest'
import { capitalize, calculateTax } from './utils';
test('capitalizes the first letter of a string', () => {
expect(capitalize('hello')).toBe('Hello');
});
test('calculates tax amount correctly', () => {
expect(calculateTax(100, 10)).toBe(10);
});
Funciones principales para organizar pruebas
Ambos frameworks proporcionan funciones para ayudarte a estructurar tus pruebas:
import { describe, test, expect } from 'vitest';
import { capitalize, calculateTax, filterItems } from './utils';
describe('String utils', () => {
test('capitalizes the first letter of a string', () => {
expect(capitalize('hello')).toBe('Hello');
expect(capitalize('world')).toBe('World');
});
test('returns empty string when input is empty', () => {
expect(capitalize('')).toBe('');
});
});
describe('Number utils', () => {
test('calculates tax amount correctly', () => {
expect(calculateTax(100, 10)).toBe(10);
expect(calculateTax(200, 5)).toBe(10);
});
});
- test() (o it()): Define un caso de prueba individual
- describe(): Agrupa pruebas relacionadas en bloques lógicos
Funciones para configuración y limpieza
Cuando necesitas preparar datos o configuraciones antes de tus pruebas:
import { describe, test, beforeEach, expect } from 'vitest';
import { filterItems } from './utils';
describe('Array filters', () => {
let items;
// Se ejecuta antes de cada prueba en este bloque
beforeEach(() => {
items = [
{ id: 1, name: 'Item 1', price: 10, inStock: true },
{ id: 2, name: 'Item 2', price: 20, inStock: false },
{ id: 3, name: 'Item 3', price: 30, inStock: true }
];
});
test('filters items by availability', () => {
const inStockItems = filterItems(items, item => item.inStock);
expect(inStockItems.length).toBe(2);
expect(inStockItems[0].id).toBe(1);
expect(inStockItems[1].id).toBe(3);
});
test('filters items by price threshold', () => {
const expensiveItems = filterItems(items, item => item.price > 15);
expect(expensiveItems.length).toBe(2);
expect(expensiveItems[0].id).toBe(2);
expect(expensiveItems[1].id).toBe(3);
});
});
- beforeEach(): Ejecuta código antes de cada prueba
- afterEach(): Ejecuta código después de cada prueba
- beforeAll() y afterAll(): Ejecutan código una sola vez antes/después de todas las pruebas
Matchers: Verificación de resultados
Los "matchers" son funciones que te permiten verificar valores de diferentes maneras:
test('demonstration of common matchers', () => {
// Igualdad
expect(2 + 2).toBe(4); // Igualdad estricta (===)
expect({ name: 'test' }).toEqual({ name: 'test' }); // Igualdad profunda
// Arrays
expect([1, 2, 3]).toContain(2); // Contiene elemento
expect([1, 2, 3]).toHaveLength(3); // Tiene longitud específica
// Números
expect(10).toBeGreaterThan(5); // Mayor que
expect(5).toBeLessThanOrEqual(5); // Menor o igual que
// Strings
expect('hello world').toMatch(/world/); // Coincide con regex
expect('hello').toContain('ell'); // Contiene substring
});
Ejecución de pruebas
Para ejecutar tus pruebas, puedes configurar scripts en tu package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
Y luego ejecutarlos con:
npm test # Modo observador: ejecuta pruebas cuando los archivos cambian
npm run test:run # Ejecuta las pruebas una vez
Ejemplo:
Crea los siguientes archivos en la carpeta testing-demo para realizar la prueba:
Calculator.js
// Función para sumar dos números
export function sum(a, b) {
return a + b;
}
// Función para restar dos números
export function subtract(a, b) {
return a - b;
}
// Función para verificar si un número es par
export function isEven(number) {
return number % 2 === 0;
}
calculator.test.js
import { expect, test, describe } from 'vitest';
import { sum, subtract, isEven } from './calculator.js';
describe('Calculadora básica', () => {
test('suma 1 + 2 igual a 3', () => {
expect(sum(1, 2)).toBe(3);
});
test('resta 5 - 3 igual a 2', () => {
expect(subtract(5, 3)).toBe(2);
});
test('verifica si un número es par', () => {
expect(isEven(4)).toBe(true);
expect(isEven(7)).toBe(false);
});
});
Ejecuta npm test
Terminología: pruebas, fixtures, aserciones
Para dominar el testing en JavaScript es importante familiarizarse con algunos términos clave. Estos conceptos te ayudarán a comunicarte efectivamente con otros desarrolladores y a entender mejor la documentación de las herramientas de pruebas.
Conceptos básicos de pruebas
Aserciones (Assertions): Son declaraciones que verifican si un valor cumple con ciertas condiciones. Forman el núcleo de cualquier prueba.
test('capitalize function works correctly', () => {
// Esto es una aserción
expect(capitalize('hello')).toBe('Hello');
});
Suite de pruebas: Un grupo de pruebas relacionadas, generalmente implementado con la función describe
.
describe('String utility functions', () => {
test('capitalize works with regular strings', () => {
// ...
});
test('capitalize handles empty strings', () => {
// ...
});
});
Cobertura de código: Mide qué porcentaje de tu código está siendo probado. Aunque no debes obsesionarte con alcanzar el 100%, es un buen indicador de qué tan completas son tus pruebas.
# Ejecutar pruebas con análisis de cobertura
npm test -- --coverage
Preparación de datos para pruebas
Cuando escribes muchas pruebas, a menudo necesitas crear datos de ejemplo repetidamente. Hay varias formas de hacerlo:
1. Datos de prueba simples
test('filterItems works with predicate', () => {
const items = [
{ id: 1, price: 10, inStock: true },
{ id: 2, price: 20, inStock: false },
{ id: 3, price: 5, inStock: true }
];
const cheap = filterItems(items, item => item.price < 15);
expect(cheap.length).toBe(2);
expect(cheap[0].id).toBe(1);
expect(cheap[1].id).toBe(3);
});
2. Funciones auxiliares
Cuando necesitas crear datos similares en múltiples pruebas, puedes usar funciones auxiliares:
function createItem(overrides = {}) {
return {
id: 1,
name: 'Test Item',
price: 10,
inStock: true,
...overrides
};
}
function createItemList() {
return [
createItem({ id: 1, name: 'Item 1' }),
createItem({ id: 2, name: 'Item 2', price: 20 }),
createItem({ id: 3, name: 'Item 3', inStock: false })
];
}
test('filters in-stock items', () => {
const items = createItemList();
const inStockItems = filterItems(items, item => item.inStock);
expect(inStockItems.length).toBe(2);
});
test('filters by price range', () => {
const items = createItemList();
const affordableItems = filterItems(items, item => item.price <= 15);
expect(affordableItems.length).toBe(2);
});
Este enfoque, a veces llamado "fixtures", te permite mantener tus pruebas más limpias y mantener la consistencia en tus datos de prueba.
Aislamiento en pruebas
Para pruebas unitarias efectivas, a menudo necesitas aislar la función que estás probando de sus dependencias:
// En vez de probar esta función que tiene dependencias externas...
function getUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json());
}
// ...podemos simular la dependencia para pruebas más controladas
import { vi } from 'vitest';
// Reemplazar temporalmente fetch con una implementación simulada
global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve({ id: 1, name: 'Test User' })
});
test('getUserData returns user information', async () => {
const user = await getUserData(1);
expect(user.name).toBe('Test User');
});
Este concepto de simulación (conocido como "mocking") es una técnica avanzada que exploraremos más a fondo en lecciones posteriores.
Ejemplo integrador: Mejorando nuestras pruebas
Veamos cómo podríamos evolucionar las pruebas para nuestra función filterItems
:
import { describe, test, expect } from 'vitest';
import { filterItems } from './utils';
describe('filterItems function', () => {
// Paso 1: Prueba básica
test('filters items with a basic predicate', () => {
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = filterItems(numbers, n => n % 2 === 0);
expect(evenNumbers).toEqual([2, 4]);
});
// Paso 2: Añadir más casos de prueba
test('works with empty arrays', () => {
const emptyArray = [];
const result = filterItems(emptyArray, item => true);
expect(result).toEqual([]);
});
test('works with complex objects', () => {
const users = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 17 },
{ id: 3, name: 'Charlie', age: 30 }
];
const adults = filterItems(users, user => user.age >= 18);
expect(adults.length).toBe(2);
expect(adults[0].name).toBe('Alice');
expect(adults[1].name).toBe('Charlie');
});
// Paso 3: Probar comportamiento con predicados inválidos
test('returns empty array when predicate is always false', () => {
const items = [1, 2, 3];
const result = filterItems(items, () => false);
expect(result).toEqual([]);
});
// Paso 4: Probar borde de error
test('throws error when predicate is not a function', () => {
const items = [1, 2, 3];
expect(() => filterItems(items, 'not a function')).toThrow();
});
});
Este ejemplo muestra cómo podemos partir de una prueba simple y gradualmente expandirla para cubrir más casos y comportamientos límite, mejorando la robustez de nuestras pruebas.
Integración con tu flujo de trabajo
Las pruebas no deben ser una actividad aislada, sino una parte integral de tu proceso de desarrollo:
- Escribe pruebas junto con tu código - No las dejes para el final
- Ejecuta pruebas frecuentemente - Idealmente con cada cambio
- Arregla las pruebas fallidas inmediatamente - No dejes que se acumulen
Conexión con otras prácticas de calidad
Las pruebas se complementan perfectamente con otras prácticas que hemos visto:
- ESLint detecta errores sintácticos y de estilo
- Las pruebas verifican que el comportamiento funcional es correcto
- La modularización (módulos ES6) hace que tu código sea más fácil de probar
Juntas, estas herramientas forman una red de seguridad que mejora significativamente la calidad de tu código y reduce los errores en producción.
Otras 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
Métodos De Strings
Sintaxis
Funciones Cierre (Closure)
Sintaxis
Operadores Avanzados
Sintaxis
Funciones
Sintaxis
Expresiones Regulares
Sintaxis
Estructuras De Control
Sintaxis
Arrays Y Métodos
Estructuras De Datos
Conjuntos Con Set
Estructuras De Datos
Mapas Con Map
Estructuras De Datos
Conjuntos Con Set
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
Funciones Flecha
Programación Funcional
Transformación Con Map()
Programación Funcional
Inmutabilidad Y Programación Funcional Pura
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
This Y Contexto
Programación Orientada A Objetos
Patrón De Módulos Y Namespace
Programación Orientada A Objetos
Prototipos Y Cadena De Prototipos
Programación Orientada A Objetos
Destructuring De Objetos Y Arrays
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
Localstorage Y Sessionstorage
Dom
Bom (Browser Object Model)
Dom
Callbacks
Programación Asíncrona
Promises
Programación Asíncrona
Async / Await
Programación Asíncrona
Api Fetch
Programación Asíncrona
Naturaleza De Js Y Event Loop
Programación Asíncrona
Websockets
Programación Asíncrona
Módulos En Es6
Construcción
Configuración De Bundlers Como Vite
Construcción
Eslint Y Calidad De Código
Construcción
Npm Y Dependencias
Construcción
Introducción A Pruebas En Js
Testing
Pruebas Unitarias
Testing
Ejercicios de programación de JavaScript
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.
Excepciones
Transformación con map()
Arrays y Métodos
Reto Métodos de Strings
Transformación con map()
Funciones flecha
Selección de elementos DOM
API Fetch
Encapsulación
Mapas con Map
Creación y uso de variables
Polimorfismo
Reto Funciones flecha
Tipos de datos
Reto Operadores avanzados
Reto Estructuras de control
Estructuras de control
Pruebas unitarias
Inmutabilidad y programación funcional pura
Funciones flecha
Polimorfismo
Reto Polimorfismo
Array
Transformación con map()
Reto Variables
Gestor de tareas con JavaScript
Proyecto Modificación de elementos DOM
Manipulación DOM
Funciones
Conjuntos con Set
Reto Prototipos y cadena de prototipos
Reto Encapsulación
Funciones flecha
Async / Await
Reto Excepciones
Reto Filtrado con filter() y find()
Reto Promises
Creación y uso de variables
Excepciones
Promises
Funciones cierre (closure)
Reto Herencia
Herencia
Reto Async / Await
Proyecto Eventos del DOM
Herencia
Selección de elementos DOM
Modificación de elementos DOM
Reto Clases y objetos
Filtrado con filter() y find()
Funciones cierre (closure)
Reto Destructuring de objetos y arrays
Callbacks
Funciones
Mapas con Map
Reducción con reduce()
Callbacks
Manipulación DOM
Introducción al DOM
Reto Funciones
Reto Funciones cierre (closure)
Promises
Reto Reducción con reduce()
Async / Await
Reto Estructuras de control
Eventos del DOM
Introducción a JavaScript
Async / Await
Promises
Selección de elementos DOM
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
Proyecto carrito compra agoodshop
Introducción a JavaScript
Reto Mapas con Map
Funciones
Proyecto administrador de contactos
Reto Expresiones regulares
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
Proyecto Manipulación DOM
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