Inicio / Artículos

Pruebas unitarias en JavaScript con Jest y Vitest: prácticas que funcionan a la larga

Pruebas unitarias en JavaScript que funcionan bien: qué pertenece a la capa de unidades, Vitest vs Jest vs Mocha, el método arrange-act-assert, mocks, casos límite, CI y dónde ayuda la IA.

Editor: REACTAPP.TOP7 min de lectura1573 palabras

Fuentes: JavaScript Unit Testing Best Practices: A Practical Guide with Jest and Vitest Examples

Las pruebas unitarias aíslan las partes más pequeñas que se pueden probar: funciones, métodos y clases, y verifican que funcionen correctamente. Se encuentran en la base de la pirámide de pruebas, por lo que deben reflejar la lógica empresarial real y los riesgos reales, no meras formalidades.

Basándonos en las prácticas habituales del sector, aquí hay una guía práctica para realizar pruebas unitarias sólidas en JavaScript.

¿Qué hace que una prueba unitaria sea buena?

Las buenas pruebas unitarias verifican los resultados, no la implementación privada. Esto mantiene estable el conjunto de pruebas cuando se refactorean sus componentes internos.

Características que aparecen una y otra vez:

  • Rápidas — en milisegundos. Los conjuntos de pruebas lentos dejan de ejecutarse antes de hacer el commit.
  • Aisladas — sin dependencia del estado de pruebas anteriores; se pueden mezclar o ejecutar en paralelo sin problemas.
  • Deterministas — mismo resultado en cada ejecución. Un resultado positivo local pero negativo en CI indica inestabilidad, y la inestabilidad destruye la confianza.
  • Legible — el conjunto de herramientas funciona como documentos dinámicos. Un principiante debería poder aprender el comportamiento esperado únicamente a partir de las pruebas.
  • Ejemplo de estructura:

    // A clean, predictable unit test using Jest/Vitest syntax
    describe('calculateTotalWithTax', () => {
      it('should apply a standard 10% tax rate to the subtotal', () => {
        // Arrange
        const subtotal = 100;
        const taxRate = 0.10;
    
        // Act
        const result = calculateTotalWithTax(subtotal, taxRate);
    
        // Assert
        expect(result).toBe(110);
      });
    
      it('should throw an explicit error if the subtotal is negative', () => {
        expect(() => calculateTotalWithTax(-50, 0.10)).toThrow('Invalid subtotal');
      });
    
      it('should return the subtotal unchanged when tax rate is 0', () => {
        // Edge case: no tax applied
        expect(calculateTotalWithTax(100, 0)).toBe(100);
      });
    
      it('should round to 2 decimal places to avoid floating-point errors', () => {
        // Guards against bugs like 19.99 * 1.10 = 21.989000000000004
        expect(calculateTotalWithTax(19.99, 0.10)).toBe(21.99);
      });
    });
    

    Qué debe incluirse (y qué no)

    Mantenga las pruebas unitarias puras para que una barra roja indique que la lógica de negocio ha fallado. Todo lo demás pertenece a niveles superiores de la pirámide.

    Estructura de la pirámide: muchas pruebas unitarias rápidas en la base, menos pruebas de integración en el medio y una capa E2E delgada en la parte superior. Invertirla genera conjuntos de pruebas lentos, inestables y costosos.

    Qué debe incluirse en las pruebas unitarias

    Ninguna operación de E/S, sin efectos secundarios y sin dependencias que no se controlen:

    • Funciones puras (cálculos fiscales, formateadores, analizadores de fechas, motores de descuentos)
    • Transformaciones (map/filter/sort, API → view-model)
    • Reglas de negocio (validación, niveles de precios, requisitos de elegibilidad); los casos límite se manejan aquí y las pruebas unitarias sirven como solución económica
    • Estado de la interfaz de usuario aislado, definido por props/state, independiente del DOM circundante o de un backend en tiempo real

    Si puedes decir “dado este input, se espera ese output” sin necesidad de red, sistema de archivos o un servicio en ejecución, entonces se trata de una prueba unitaria. Examina exhaustivamente los casos límite: la idea es que estas pruebas sean rápidas.

    Evitarlo

    Los equipos pierden tiempo realizando pruebas unitarias en la capa incorrecta:

    • Bibliotecas de terceros (Lodash sort, Axios headers): tú no eres su CI
    • Conexiones y consultas a la base de datos: incluso SQLite en memoria se considera pruebas de integración
    • Operaciones complejas en red (MSW + reintentos + verificaciones de encabezados): territorio de pruebas de integración
    • Cualquier cosa que requiera Docker, puertos abiertos o “iniciar primero compose”

    Prueba rápida: desconecta Internet. Si la prueba falla, no es una prueba unitaria.

    Elegir un framework de JavaScript

    Asocie la herramienta con el pipeline de construcción; optimice la experiencia del desarrollador en lugar de buscar lo más moderno. Una solución con poca configuración es mejor que perseguir el runner más reciente.

    Para la mayoría de las aplicaciones modernas, la elección es Vitest frente a Jest.

    Vitest: configuración por defecto sólida con Vite

    Desarrollado sobre Vite, reutiliza la configuración y admite TypeScript y ESM nativo. El modo de inspección y la ejecución suelen ser más rápidos que los runners antiguos, y su API similar a Jest facilita la migración.

    Jest: opción por defecto en muchos pipelines de React

    En Webpack, Next.js o stacks antiguos al estilo CRA, Jest es la opción segura todo en uno: afirmaciones, mocks, cobertura y capturas de estado. Es más pesado en repositorios enormes y ESM es menos práctico que Vitest, pero su madurez y ecosistema siguen siendo ventajas para muchos equipos.

    Mocha: stacks modulares

    Útil en backends de Node cuando desea armar Chai + Sinon por sí mismo. Más configuración, más control.

    Elegidor rápido

    • Proyecto Vite → Vitest
    • React con Webpack / Next / CRA → Jest
    • APIs de Node que requieren una pila modular → Mocha

    Prácticas (al estilo de Jest/Vitest)

    Busque pruebas legibles, deterministas y seguras para refactorizaciones.

    1. Arrange–Act–Assert

    Estructura idéntica en cada ocasión:

    import { describe, it, expect } from 'vitest';
    import { add } from './math';
    
    describe('add()', () => {
      it('returns the sum of two numbers', () => {
        // Arrange
        const a = 2;
        const b = 3;
        // Act
        const result = add(a, b);
        // Assert
        expect(result).toBe(5);
      });
    });
    

    2. Nombres que describen el comportamiento

    Los bloques anidados describe / it al estilo BDD deben leerse como oraciones:

    describe('CartService', () => {
      describe('when adding an item', () => {
        it('increases the total cart quantity by one', () => {
          // Test code...
        });
    
        it('updates the final checkout price', () => {
          // Test code...
        });
      });
    });
    

    El ejecutor muestra una oración legible que indica el servicio, la situación y el cambio de cantidad esperado.

    3. Fixtures compartidos, no valores literales dispersos

    const testUser = {
      name: 'Alice',
      age: 30,
    };
    it('returns the user name', () => {
      expect(getUserName(testUser)).toBe('Alice');
    });
    

    Cambie los datos de ejemplo en un solo lugar.

    4. Enfoque en el comportamiento, no en la implementación

    Verifique las salidas, no qué ayuda privada se ejecutó. Las pruebas vinculadas a la implementación fallan con refactorizaciones inofensivas.

    5. Simule elementos externos con cuidado

    Stub de APIs/bases de datos/terceros para mayor velocidad y determinismo. Tipos:

    • Stubs que controlan las entradas
    • Spies que vigilan las interacciones
    • Mocks cuando la interacción es la afirmación a verificar
    • Fakes como sustitutos funcionales de tamaño reducido

    Uso de stubs con fetch:

    import { vi } from 'vitest';
    
    global.fetch = vi.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve({ name: 'Alice' }),
      })
    );
    

    No uses mocks para tu propia lógica de negocio a menos que sea necesario; el exceso de mocks oculta defectos.

    6. Casos límite y errores

    Límites, valores nulos, cadenas vacías, argumentos inválidos. Cubre valores mínimos/máximos y cercanos a ellos, colecciones vacías, campos faltantes, tipos incorrectos, valores negativos y extremos enteros.

    7. Independencia

    Cualquier orden, cualquier programación en paralelo. Los fixtures mutables compartidos generan efectos en cadena y fallos impredecibles. Prefiere usar beforeEach para mantener un estado fresco.

    8. Integración continua en cada PR

    Ejecuta el conjunto de pruebas unitarias en la integración continua para que las regresiones impidan la fusión y los tests omitidos no puedan ocultar problemas.

    El papel de la IA y el lugar que ocupan los humanos

    Los LLM aceleran la creación de textos genéricos, variantes repetitivas y plantillas preliminares. Al introducir funciones puras y criterios de aceptación en un modelo, se pueden elaborar conjuntos amplios de contenido, ocultar casos límite y generar datos de frontera sintéticos que los humanos suelen pasar por alto debido a plazos ajustados.

    Los modelos aún carecen de juicio arquitectónico y de conocimiento específico del dominio. Flujo de trabajo útil: dejar que la IA elabore todos los casos límite posibles y luego revisar cada afirmación en relación con los requisitos reales antes de integrarla. Usted se convierte en el editor de lo “correcto”; el modelo escribe rápidamente.

    Sus propias experiencias con pruebas unitarias perfeccionarán aún más este proceso: trate las prácticas mencionadas como un punto de partida, no como una lista completa ya terminada.

    Crear un conjunto básico sin abrumarse

    Comience con funciones puras del dominio: cálculos financieros, verificación de requisitos y analizadores de datos. Escriba pruebas AAA con nombres descriptivos del comportamiento y configuraciones compartidas. Añada casos límite hasta que la matriz de “null / vacío / máximo” parezca aburrida. Solo entonces proceda a modificar los componentes de interfaz, y únicamente para el comportamiento prop→render que no requiere un navegador.

    Cuando una prueba necesite una base de datos o Docker, transfórmela deliberadamente a la carpeta de integración. Mantener estricta la carpeta de pruebas unitarias preserva el acuerdo de que una barra roja indica que la lógica falló.

    En cuanto a la elección del framework, no migre de Jest a Vitest en medio del desarrollo por mera moda. Si ya está utilizando Vite, Vitest es la opción natural. Si el monorepo se basa en Jest con convenciones de Next.js, quédese y invierta en mejorar la velocidad en otros aspectos (dividir las pruebas de CI, eliminar snapshots innecesarios).

    Rituales de revisión que mantienen las pruebas honestas

    En la revisión de PR, rechazar las pruebas que verifiquen el número de llamadas a métodos privados. Rechazar los fixtures mutables compartidos entre archivos. Exigir que las pruebas unitarias den resultado positivo en CI antes de realizar la fusión. Eliminar periódicamente las pruebas que solo repiten el comportamiento de la biblioteca de simulación.

    Al utilizar IA para redactar conjuntos de pruebas, pegar la función pura y los criterios de aceptación, y luego eliminar las afirmaciones que contengan requisitos inventados. Los seres humanos siguen decidiendo qué significa “correcto”; los modelos simplemente escriben más rápido.

    Recordatorio final

    Las pruebas unitarias son un seguro económico siempre que sean rápidas, estén aisladas y se centren en los resultados. Proteger ese espacio específico. Dejar que las pruebas de integración y E2E se encarguen del resto de la pirámide, para que la base nunca se convierta en un entorno de producción en miniatura.

    Catálogo concreto de casos límite que pueden servir de referencia

    Para APIs numéricas: cero, uno, el valor entero máximo, valores negativos, NaN si aplica, y valores un paso más allá del límite documentado. Para cadenas de texto: vacía, con solo espacios en blanco, Unicode, extremadamente larga. Para colecciones: vacía, con un único elemento, duplicados, ya ordenada o en orden inverso. Para objetos: claves faltantes, campos anidados nulos, claves adicionales inesperadas si el analizador es estricto.

    Escribe esos casos una vez en formato de tabla cuando el ejecutor lo permita. El catálogo se convierte en conocimiento compartido del equipo y evita las PRs que solo cubren escenarios ideales.

    Snapshots con precaución

    Los snapsshots de Jest son adecuados para resultados visuales estables, pero peligrosos para marcado volátil. Prefiere afirmaciones explícitas sobre roles y texto mediante Testing Library para componentes interactivos. Si mantienes snapsshots, revisa las diferencias como si fueran código de producción, no como ruido que se puede ignorar.

    Sugerencias para CI paralelo

    Divide los archivos en partes, falla rápidamente ante la primera prueba de unidad que dé error, y mantén el tiempo de ejecución de las pruebas unitarias por debajo de dos minutos. Si los procesos CI de pruebas unitarias superan regularmente ese límite, o bien tienes demasiadas pruebas de integración etiquetadas erróneamente como pruebas unitarias, o bien existe algún proceso de inicio del entorno oculto en los archivos setupFiles.

    Haz un seguimiento separado de la tasa de fallos impredecibles. Un conjunto de pruebas que solo muestra resultado positivo después de dos ejecuciones repetidas no se considera exitoso.