JavaScript

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:

  1. Reutilizar configuraciones comunes entre pruebas
  2. Mantener las pruebas deterministas
  3. 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:

  1. Red: Escribir una prueba que falle
  2. Green: Implementar el código mínimo para que la prueba pase
  3. 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.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende JavaScript online

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

JavaScript
Puzzle

Polimorfismo

JavaScript
Test

Array

JavaScript
Código

Transformación con map()

JavaScript
Test

Gestor de tareas con JavaScript

JavaScript
Proyecto

Manipulación DOM

JavaScript
Test

Funciones

JavaScript
Test

Funciones flecha

JavaScript
Código

Async / Await

JavaScript
Código

Creación y uso de variables

JavaScript
Test

Excepciones

JavaScript
Puzzle

Promises

JavaScript
Código

Funciones cierre (closure)

JavaScript
Test

Herencia

JavaScript
Puzzle

Herencia

JavaScript
Test

Estructuras de control

JavaScript
Código

Selección de elementos DOM

JavaScript
Test

Modificación de elementos DOM

JavaScript
Test

Filtrado con filter() y find()

JavaScript
Test

Funciones cierre (closure)

JavaScript
Puzzle

Funciones

JavaScript
Puzzle

Mapas con Map

JavaScript
Test

Reducción con reduce()

JavaScript
Test

Callbacks

JavaScript
Puzzle

Manipulación DOM

JavaScript
Puzzle

Promises

JavaScript
Test

Async / Await

JavaScript
Test

Eventos del DOM

JavaScript
Puzzle

Async / Await

JavaScript
Puzzle

Promises

JavaScript
Puzzle

Filtrado con filter() y find()

JavaScript
Código

Callbacks

JavaScript
Test

Creación de clases y objetos Restaurante

JavaScript
Código

Reducción con reduce()

JavaScript
Código

Filtrado con filter() y find()

JavaScript
Puzzle

Reducción con reduce()

JavaScript
Puzzle

Conjuntos con Set

JavaScript
Puzzle

Herencia de clases

JavaScript
Código

Eventos del DOM

JavaScript
Test

Clases y objetos

JavaScript
Puzzle

Modificación de elementos DOM

JavaScript
Puzzle

Mapas con Map

JavaScript
Puzzle

Introducción a JavaScript

JavaScript
Test

Funciones

JavaScript
Código

Tipos de datos

JavaScript
Test

Clases y objetos

JavaScript
Test

Array

JavaScript
Test

Conjuntos con Set

JavaScript
Test

Array

JavaScript
Puzzle

Encapsulación

JavaScript
Puzzle

Clases y objetos

JavaScript
Código

Uso de operadores

JavaScript
Puzzle

Uso de operadores

JavaScript
Test

Estructuras de control

JavaScript
Test

Excepciones

JavaScript
Test

Transformación con map()

JavaScript
Puzzle

Funciones flecha

JavaScript
Test

Selección de elementos DOM

JavaScript
Puzzle

Encapsulación

JavaScript
Test

Mapas con Map

JavaScript
Código

Creación y uso de variables

JavaScript
Puzzle

Polimorfismo

JavaScript
Puzzle

Tipos de datos

JavaScript
Puzzle

Estructuras de control

JavaScript
Puzzle

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.

Accede GRATIS a JavaScript y certifícate

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