HDP115

Testing en JavaScript

Aprende a implementar pruebas efectivas para garantizar la calidad de tu código JavaScript, desde pruebas unitarias hasta end-to-end.

CE

Cristian Escalante

Última actualización: 24 de abril de 2025

javascript
testing
desarrollo web
calidad de software

Testing en JavaScript

¿Por qué es importante el testing?

El testing o pruebas de software es una práctica fundamental en el desarrollo que consiste en verificar que el código funciona según lo esperado. En JavaScript, el testing es particularmente importante debido a:

  • La naturaleza dinámica y débilmente tipada del lenguaje
  • La complejidad creciente de las aplicaciones web modernas
  • La necesidad de garantizar experiencias consistentes en diferentes navegadores
  • El desarrollo colaborativo en equipos
// El código sin pruebas puede parecer funcionar correctamente
// pero esconder errores sutiles que aparecen en producción
function suma(a, b) {
  return a + b;
}

// ¿Qué pasa si llamamos suma("2", 3)?
// Sin pruebas, estos casos límite pueden pasar desapercibidos

Tipos de pruebas

Pruebas unitarias

Las pruebas unitarias verifican el funcionamiento aislado de pequeñas partes de código (funciones, métodos, clases) de forma independiente.

// Función a probar
function esPalindromo(texto) {
  const textoNormalizado = texto.toLowerCase().replace(/[^a-z0-9]/g, '');
  const textoInvertido = textoNormalizado.split('').reverse().join('');
  return textoNormalizado === textoInvertido;
}

// Prueba unitaria con Jest
test('esPalindromo identifica correctamente palíndromos', () => {
  expect(esPalindromo('Ana')).toBe(true);
  expect(esPalindromo('Anita lava la tina')).toBe(true);
  expect(esPalindromo('JavaScript')).toBe(false);
});

Pruebas de integración

Las pruebas de integración verifican que diferentes unidades de código funcionan correctamente juntas.

// Prueba de integración con Jest
describe('Sistema de autenticación', () => {
  test('el proceso completo de login funciona correctamente', async () => {
    // Configurar la base de datos de prueba
    await setupTestDatabase();
    
    // Crear un usuario de prueba
    const usuario = await crearUsuario('test@example.com', 'password123');
    
    // Probar el proceso de login
    const resultado = await loginService.autenticar('test@example.com', 'password123');
    
    // Verificar que se generó un token válido
    expect(resultado.success).toBe(true);
    expect(resultado.token).toBeDefined();
    
    // Verificar que podemos acceder a recursos protegidos con ese token
    const perfilUsuario = await perfilService.obtenerPerfil(resultado.token);
    expect(perfilUsuario.email).toBe('test@example.com');
  });
});

Pruebas end-to-end (e2e)

Las pruebas e2e simulan el comportamiento de un usuario real interactuando con la aplicación completa.

// Prueba e2e con Cypress
describe('Formulario de contacto', () => {
  it('permite enviar un mensaje y muestra confirmación', () => {
    // Visitar la página
    cy.visit('/contacto');
    
    // Rellenar el formulario
    cy.get('#nombre').type('Juan Pérez');
    cy.get('#email').type('juan@example.com');
    cy.get('#mensaje').type('Este es un mensaje de prueba');
    
    // Enviar el formulario
    cy.get('button[type="submit"]').click();
    
    // Verificar que se muestra el mensaje de confirmación
    cy.get('.mensaje-exito').should('be.visible');
    cy.get('.mensaje-exito').should('contain', 'Mensaje enviado correctamente');
  });
});

Frameworks de testing populares

Jest

Jest es un framework de testing completo desarrollado por Facebook, muy popular en el ecosistema de React.

// Configuración básica de Jest
// En package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  },
  "jest": {
    "testEnvironment": "node",
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80
      }
    }
  }
}

// Ejemplo de prueba con Jest
import { suma } from './matematicas';

describe('Funciones matemáticas', () => {
  test('suma correctamente dos números', () => {
    expect(suma(1, 2)).toBe(3);
    expect(suma(-1, 1)).toBe(0);
    expect(suma(0, 0)).toBe(0);
  });
});

Mocha

Mocha es un framework flexible que permite usar diferentes bibliotecas de aserciones como Chai.

// Ejemplo de prueba con Mocha y Chai
const { expect } = require('chai');
const { suma } = require('./matematicas');

describe('Funciones matemáticas', function() {
  it('suma correctamente dos números', function() {
    expect(suma(1, 2)).to.equal(3);
    expect(suma(-1, 1)).to.equal(0);
    expect(suma(0, 0)).to.equal(0);
  });
});

Jasmine

Jasmine es un framework de BDD (Behavior-Driven Development) que no requiere dependencias externas.

// Ejemplo de prueba con Jasmine
describe('Funciones matemáticas', function() {
  it('suma correctamente dos números', function() {
    expect(suma(1, 2)).toEqual(3);
    expect(suma(-1, 1)).toEqual(0);
    expect(suma(0, 0)).toEqual(0);
  });
});

Assertions y matchers

Las aserciones (assertions) son declaraciones que verifican si una condición es verdadera. Los matchers son funciones que implementan comparaciones específicas.

Assertions básicas

// Jest/Jasmine
expect(valor).toBe(esperado);          // Igualdad estricta (===)
expect(valor).toEqual(esperado);       // Igualdad profunda de objetos
expect(valor).toBeTruthy();            // Valor evaluado como verdadero
expect(valor).toBeFalsy();             // Valor evaluado como falso
expect(array).toContain(elemento);     // Array contiene elemento
expect(objeto).toHaveProperty('prop'); // Objeto tiene propiedad

// Chai
expect(valor).to.equal(esperado);      // Igualdad (==)
expect(valor).to.deep.equal(esperado); // Igualdad profunda
expect(valor).to.be.true;              // Es literalmente true
expect(valor).to.be.ok;                // Evaluado como verdadero
expect(array).to.include(elemento);    // Array contiene elemento
expect(objeto).to.have.property('prop'); // Objeto tiene propiedad

Matchers avanzados

// Jest
expect(fn).toThrow(Error);             // Función lanza excepción
expect(valor).toMatch(/expresion/);    // Coincide con regex
expect(objeto).toMatchObject({         // Objeto contiene subconjunto
  prop: valor
});
expect(fn).toHaveBeenCalledTimes(n);   // Función llamada n veces
expect(fn).toHaveBeenCalledWith(arg);  // Función llamada con argumento

// Chai con plugins
expect(fn).to.throw(Error);
expect(valor).to.match(/expresion/);
expect(objeto).to.include({
  prop: valor
});
expect(spy).to.have.been.calledTwice;
expect(spy).to.have.been.calledWith(arg);

Mocks, stubs y spies

Estas herramientas permiten simular comportamientos y verificar interacciones en las pruebas.

Mocks

Los mocks son objetos que simulan el comportamiento de objetos reales de forma controlada.

// Mock con Jest
jest.mock('./servicioUsuarios');
import { obtenerUsuario } from './servicioUsuarios';

// Configurar el comportamiento del mock
obtenerUsuario.mockResolvedValue({
  id: 1,
  nombre: 'Usuario de prueba',
  email: 'test@example.com'
});

test('muestra el nombre de usuario', async () => {
  const componente = await renderComponente();
  expect(componente.querySelector('.nombre').textContent).toBe('Usuario de prueba');
  expect(obtenerUsuario).toHaveBeenCalledWith(1);
});

Stubs

Los stubs son objetos que reemplazan funciones específicas con comportamientos predefinidos.

// Stub con Sinon.js
const sinon = require('sinon');
const servicioClima = require('./servicioClima');

describe('Pronóstico del tiempo', () => {
  it('muestra la temperatura correctamente', async () => {
    // Crear un stub para la función obtenerTemperatura
    const stub = sinon.stub(servicioClima, 'obtenerTemperatura');
    stub.resolves(25); // La función siempre devolverá 25
    
    const resultado = await mostrarPronostico('Madrid');
    expect(resultado).to.include('25°C');
    
    // Verificar que la función fue llamada con el argumento correcto
    expect(stub.calledWith('Madrid')).to.be.true;
    
    // Restaurar la función original
    stub.restore();
  });
});

Spies

Los spies son funciones que registran información sobre cómo fueron llamadas, sin cambiar su comportamiento.

// Spy con Jest
test('registra el error correctamente', () => {
  // Crear un spy para console.error
  const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
  
  // Provocar un error
  try {
    funcionQueProduciriaError();
  } catch (error) {
    manejarError(error);
  }
  
  // Verificar que se registró el error
  expect(errorSpy).toHaveBeenCalled();
  expect(errorSpy.mock.calls[0][0]).toMatch(/Error inesperado/);
  
  // Restaurar la función original
  errorSpy.mockRestore();
});

Test Driven Development (TDD)

TDD es una metodología de desarrollo que sigue el ciclo Red-Green-Refactor:

  1. Red: Escribir una prueba que falle
  2. Green: Escribir el código mínimo para que la prueba pase
  3. Refactor: Mejorar el código manteniendo las pruebas en verde
// Ejemplo de TDD con Jest

// 1. Red: Escribir una prueba que falle
test('validarEmail identifica emails válidos', () => {
  expect(validarEmail('usuario@dominio.com')).toBe(true);
  expect(validarEmail('usuario.nombre@dominio.co.uk')).toBe(true);
  expect(validarEmail('usuario@dominio')).toBe(false);
  expect(validarEmail('usuario@.com')).toBe(false);
  expect(validarEmail('usuario@dominio.')).toBe(false);
});

// 2. Green: Implementar la función mínima para que pase
function validarEmail(email) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

// 3. Refactor: Mejorar la implementación
function validarEmail(email) {
  if (!email || typeof email !== 'string') return false;
  
  // Expresión regular más robusta para validar emails
  const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return regex.test(email);
}

Testing de componentes UI

Testing con React Testing Library

// Ejemplo de prueba de un componente React
import { render, screen, fireEvent } from '@testing-library/react';
import Contador from './Contador';

test('incrementa el contador cuando se hace clic en el botón', () => {
  // Renderizar el componente
  render(<Contador />);
  
  // Verificar el estado inicial
  expect(screen.getByText(/contador: 0/i)).toBeInTheDocument();
  
  // Simular un clic en el botón
  fireEvent.click(screen.getByRole('button', { name: /incrementar/i }));
  
  // Verificar que el contador se incrementó
  expect(screen.getByText(/contador: 1/i)).toBeInTheDocument();
});

Testing con Vue Test Utils

// Ejemplo de prueba de un componente Vue
import { mount } from '@vue/test-utils';
import Contador from './Contador.vue';

describe('Contador', () => {
  test('incrementa el contador cuando se hace clic en el botón', async () => {
    // Montar el componente
    const wrapper = mount(Contador);
    
    // Verificar el estado inicial
    expect(wrapper.text()).toContain('Contador: 0');
    
    // Simular un clic en el botón
    await wrapper.find('button').trigger('click');
    
    // Verificar que el contador se incrementó
    expect(wrapper.text()).toContain('Contador: 1');
  });
});

Cobertura de código

La cobertura de código mide qué porcentaje del código está siendo ejecutado durante las pruebas.

// Configuración de cobertura con Jest
// En package.json
{
  "scripts": {
    "test": "jest",
    "test:coverage": "jest --coverage"
  },
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx}",
      "!**/node_modules/**",
      "!**/vendor/**"
    ],
    "coverageThreshold": {
      "global": {
        "statements": 80,
        "branches": 80,
        "functions": 80,
        "lines": 80
      }
    }
  }
}

Tipos de cobertura

  • Cobertura de líneas: Porcentaje de líneas de código ejecutadas
  • Cobertura de ramas: Porcentaje de ramas de decisión (if/else) ejecutadas
  • Cobertura de funciones: Porcentaje de funciones llamadas
  • Cobertura de declaraciones: Porcentaje de declaraciones ejecutadas

Testing de aplicaciones asíncronas

Promesas

// Testing de promesas con Jest
test('obtenerDatos devuelve los datos correctos', () => {
  return obtenerDatos().then(datos => {
    expect(datos).toEqual({
      id: 1,
      nombre: 'Producto de prueba'
    });
  });
});

// Alternativa con async/await
test('obtenerDatos devuelve los datos correctos', async () => {
  const datos = await obtenerDatos();
  expect(datos).toEqual({
    id: 1,
    nombre: 'Producto de prueba'
  });
});

Callbacks

// Testing de callbacks con Jest
test('procesarDatos llama al callback con el resultado correcto', done => {
  procesarDatos(resultado => {
    try {
      expect(resultado).toBe('Datos procesados');
      done();
    } catch (error) {
      done(error);
    }
  });
});

Temporizadores

// Testing de temporizadores con Jest
test('ejecuta la función después del tiempo especificado', () => {
  jest.useFakeTimers();
  
  const callback = jest.fn();
  ejecutarDespuesDe(callback, 1000);
  
  // Verificar que el callback no se ha llamado aún
  expect(callback).not.toHaveBeenCalled();
  
  // Avanzar el tiempo 1 segundo
  jest.advanceTimersByTime(1000);
  
  // Verificar que el callback se llamó
  expect(callback).toHaveBeenCalled();
  
  jest.useRealTimers();
});

Mejores prácticas de testing

  1. Pruebas independientes: Cada prueba debe funcionar de forma aislada sin depender del resultado de otras pruebas.
  2. Pruebas deterministas: Las pruebas deben producir siempre el mismo resultado bajo las mismas condiciones.
  3. Pruebas rápidas: Las pruebas deben ejecutarse rápidamente para permitir ciclos de desarrollo ágiles.
  4. Pruebas legibles: El código de las pruebas debe ser claro y expresar la intención de lo que se está probando.
  5. Pruebas mantenibles: Evitar duplicación de código en las pruebas mediante helpers y fixtures.
  6. Pirámide de pruebas: Mantener una proporción adecuada entre tipos de pruebas:
    • Muchas pruebas unitarias (base de la pirámide)
    • Algunas pruebas de integración (medio)
    • Pocas pruebas e2e (cima)
// Ejemplo de una prueba bien estructurada
describe('Servicio de autenticación', () => {
  // Configuración común
  let authService;
  let mockUsuarioRepository;
  
  beforeEach(() => {
    // Configurar mocks y dependencias
    mockUsuarioRepository = {
      findByEmail: jest.fn(),
      save: jest.fn()
    };
    
    authService = new AuthService(mockUsuarioRepository);
  });
  
  describe('registro', () => {
    test('registra un nuevo usuario correctamente', async () => {
      // Configurar el comportamiento del mock
      mockUsuarioRepository.findByEmail.mockResolvedValue(null);
      mockUsuarioRepository.save.mockResolvedValue({
        id: 1,
        email: 'nuevo@example.com',
        password: expect.any(String) // Password hasheado
      });
      
      // Ejecutar la función a probar
      const resultado = await authService.registrar('nuevo@example.com', 'password123');
      
      // Verificar el resultado
      expect(resultado.success).toBe(true);
      expect(resultado.usuario.email).toBe('nuevo@example.com');
      
      // Verificar interacciones con las dependencias
      expect(mockUsuarioRepository.findByEmail).toHaveBeenCalledWith('nuevo@example.com');
      expect(mockUsuarioRepository.save).toHaveBeenCalled();
      expect(mockUsuarioRepository.save.mock.calls[0][0].password).not.toBe('password123');
    });
    
    test('falla si el email ya está registrado', async () => {
      // Configurar el comportamiento del mock
      mockUsuarioRepository.findByEmail.mockResolvedValue({
        id: 1,
        email: 'existente@example.com'
      });
      
      // Ejecutar y verificar que lanza error
      await expect(
        authService.registrar('existente@example.com', 'password123')
      ).rejects.toThrow('Email ya registrado');
      
      // Verificar que no se intentó guardar
      expect(mockUsuarioRepository.save).not.toHaveBeenCalled();
    });
  });
});

Integración continua y testing

La integración continua (CI) automatiza la ejecución de pruebas cada vez que se realiza un cambio en el código.

# Ejemplo de configuración de GitHub Actions para ejecutar pruebas
# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
        
    - name: Install dependencies
      run: npm ci
      
    - name: Run tests
      run: npm test
      
    - name: Upload coverage report
      uses: codecov/codecov-action@v2
      with:
        token: ${{ secrets.CODECOV_TOKEN }}

Conclusión

El testing es una parte fundamental del desarrollo de software profesional. En JavaScript, con su ecosistema rico en herramientas y frameworks, podemos implementar estrategias de pruebas robustas que mejoran la calidad del código y facilitan el mantenimiento a largo plazo.

Las pruebas bien diseñadas no solo previenen errores, sino que también documentan el comportamiento esperado del código y permiten refactorizar con confianza. Invertir tiempo en aprender y aplicar buenas prácticas de testing es una de las mejores decisiones que un desarrollador puede tomar para mejorar su código y su proceso de desarrollo.

Patrones de Diseño en JavaScript
Aprende sobre los patrones de diseño más utilizados en JavaS...
Optimización y Rendimiento en JavaScript
Aprende técnicas avanzadas para mejorar la velocidad y efici...
Referencias
Yoni Goldberg. JavaScript Testing Best Practices. https://github.com/goldbergyoni/javascript-testing-best-practices
Facebook Open Source. Jest Documentation. https://jestjs.io/docs/getting-started
Lucas da Costa. Testing JavaScript Applications. https://www.manning.com/books/testing-javascript-applications

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de Python

Aprende los conceptos básicos de Python

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025