Testing con Vitest
Aprende a escribir tests automaticos en JavaScript con Vitest, el test runner moderno que se integra de forma nativa con Vite y Astro.
1 ¿Qué es testing y por qué deberías hacerlo?
Hacer testing es escribir código que verifica que otro código funciona. En lugar de abrir el navegador y probar manualmente cada cambio, dejas que un programa lo haga por ti, miles de veces, en segundos.
🔬 Analogía del mundo real:
Los tests son como los controles de calidad en una fábrica de coches. Antes de salir al mercado, cada coche pasa por una línea de pruebas: frenos, motor, luces, airbag. Tus tests son esa línea: cada vez que tocas el código, automáticamente verificas que nada se rompió.
¿Para qué sirven?
Detectar bugs antes que el usuario
Si un test falla en tu máquina, no llega a producción.
Refactorizar sin miedo
Si los tests pasan después del cambio, la lógica sigue intacta.
Documentar cómo funciona el código
Un test es un ejemplo ejecutable. Lees el test y entiendes qué hace la función.
Iterar más rápido
Sin tests, cada cambio te obliga a probar la app entera a mano. Con tests, milisegundos.
2 Tipos de test que existen
Hay dos formas habituales de clasificar los tests: según cuánto sabes del código interno y según el nivel de integración.
2.1. Por conocimiento del código: caja negra, caja blanca y caja gris
Imagina la función que vas a testear como una caja. La pregunta es: ¿puedes ver lo que hay dentro?
Test de caja negra
Solo conoces inputs y outputs. No ves el codigo interno.
suma(2, 3)5suma(2, 3) === 5,suma(-1, 1) === 0. No sabes que validaciones internas existen.Caja negra (black box)
No conoces la implementación. Tests basados en contrato: input X → output Y.
Útil para QA externo, testear APIs públicas, librerías de terceros.
Caja gris (gray box)
Conoces parcialmente el interior. Tests con conocimiento parcial de la implementación.
Habitual en integración: sabes la arquitectura pero no cada línea.
Caja blanca (white box)
Ves todo el código. Tests que cubren cada rama, condición y línea.
Lo que escribes tú como desarrollador sobre tu propio código.
2.2. Por nivel de integración: la pirámide de tests
No todos los tests son iguales. Hay tres niveles: unitarios, de integración y end-to-end. La regla de oro: muchos abajo, pocos arriba.
Piramide de Tests
Muchos unitarios rapidos en la base, pocos E2E lentos en la cima.
Unit tests (unitarios)
Testean una función o módulo aislado. Son rápidos (milisegundos), baratos y fáciles de mantener.
Ejemplo: testear una función formatPrice(1500).
Integration tests (integración)
Verifican que varias unidades trabajan bien juntas. Más lentos (segundos) y más caros. Ejemplo: testear un endpoint que llama a un servicio que llama a la BD.
E2E tests (end-to-end)
Simulan al usuario real usando la app entera en un navegador. Lentos (minutos) y caros. Para esto NO se usa Vitest sino Playwright o Cypress.
2.3. Bonus: TDD (Test-Driven Development)
TDD es una metodología donde escribes el test ANTES que el código. Suena raro, pero es brutal para forzarte a pensar la API antes de implementarla.
Ciclo TDD: Red → Green → Refactor
El ciclo de Test-Driven Development. Repítelo en bucle.
No es obligatorio. TDD es una técnica más, no un dogma. Para empezar, escribe tests después de tu código. Con experiencia, decides cuándo aplicar TDD (útil en lógica compleja, refactorings grandes, bugfixes).
3 ¿Qué es Vitest y por qué usarlo?
Vitest es un test runner moderno construido sobre Vite. Es lo que vas a usar para escribir y ejecutar tests en proyectos Astro, Vue, React, Svelte... cualquier cosa que use Vite por debajo (que es prácticamente todo el ecosistema moderno).
Súper rápido
Comparte el pipeline de Vite. No reinventa transpilación: es la misma máquina que sirve tu app.
API idéntica a Jest
describe / it / expect. Si sabes Jest, sabes Vitest.
ESM y TypeScript out-of-the-box
Sin Babel, sin ts-jest, sin configuraciones de pesadilla.
Watch mode instantáneo
Cambias un archivo, solo se re-ejecutan los tests afectados. Feedback en milisegundos.
UI opcional
vitest --ui abre un dashboard web con árbol de tests, logs y filtros.
4 Instalación
Como toda librería profesional, se instala con npm:
1npm install -D vitest
La flag -D (o --save-dev)
la guarda en devDependencies del package.json.
Es una dependencia de desarrollo: en producción no se instala.
Añade los scripts a tu package.json:
1{2 "scripts": {3 "test": "vitest",4 "test:run": "vitest run",5 "test:ui": "vitest --ui",6 "coverage": "vitest run --coverage"7 }8}npm test Watch mode (re-ejecuta al guardar)
npm run test:run Ejecuta una vez (CI/CD)
npm run test:ui Dashboard web con UI
npm run coverage Reporte de cobertura
5 Tu primer test paso a paso
Vamos a testear una función simple: una calculadora con una función suma.
Crear el código a testear
Crea src/utils/calculadora.js:
1export function suma(a, b) {2 if (typeof a !== 'number' || typeof b !== 'number') {3 throw new Error('Ambos parametros deben ser numeros');4 }5 return a + b;6}Crear el archivo de test
Crea src/utils/calculadora.test.js.
La convención es mismo nombre + .test.js (Vitest los detecta automáticamente).
1import { describe, it, expect } from 'vitest';2import { suma } from './calculadora.js';3 4describe('suma', () => {5 it('devuelve la suma de dos numeros positivos', () => {6 expect(suma(2, 3)).toBe(5);7 });8 9 it('funciona con numeros negativos', () => {10 expect(suma(-1, -1)).toBe(-2);11 });12 13 it('lanza un error si recibe un string', () => {14 expect(() => suma('hola', 2)).toThrow('Ambos parametros');15 });16});Ejecutar los tests
1npm testVerás algo así en la terminal:
1 ✓ src/utils/calculadora.test.js (3)2 ✓ suma3 ✓ devuelve la suma de dos numeros positivos4 ✓ funciona con numeros negativos5 ✓ lanza un error si recibe un string6 7 Test Files 1 passed (1)8 Tests 3 passed (3)9 Duration 142msQue un test falle es bueno
Cambia un valor esperado a propósito (por ejemplo .toBe(99)) y mira la salida.
Vitest te muestra exactamente qué esperabas vs qué obtuviste:
1 ✗ suma > devuelve la suma de dos numeros positivos2 AssertionError: expected 5 to be 993 - Expected: 994 + Received: 55 6 ❯ src/utils/calculadora.test.js:5:24💡 Resumen del flujo:
- Crea tu función en
archivo.js - Crea tests en
archivo.test.js npm testen watch mode mientras desarrollas
6
Anatomía: describe, it y expect
Todo test de Vitest se construye con estas 3 funciones:
describe(nombre, fn)
Agrupa varios tests bajo un mismo paraguas. Sirve para organizar visualmente los resultados.
Puedes anidar describe dentro de otro.
1describe('Calculadora', () => {2 describe('suma', () => {3 it('suma dos positivos', () => { /* ... */ });4 });5 describe('resta', () => {6 it('resta dos positivos', () => { /* ... */ });7 });8});it(nombre, fn) o test(nombre, fn)
Define un test concreto. Son sinónimos: it es más legible
("it should...") y test es más explícito.
1it('devuelve 5 cuando sumo 2 + 3', () => {2 expect(suma(2, 3)).toBe(5);3});expect(valor).matcher(esperado)
La afirmación. Compara el valor real con el esperado usando un matcher
(la función después del punto: .toBe(), .toEqual(), etc).
1expect(suma(2, 3)).toBe(5); // estricta igualdad2expect({a: 1}).toEqual({a: 1}); // igualdad profunda3expect([1, 2, 3]).toContain(2); // contenido4expect(() => fn()).toThrow(); // lanza error7 Matchers más usados
Estos son los matchers que vas a usar el 90% del tiempo:
| Matcher | Para qué sirve | Ejemplo |
|---|---|---|
| .toBe(x) | Igualdad estricta (===) para primitivos | expect(2+2).toBe(4) |
| .toEqual(x) | Igualdad profunda para objetos y arrays | expect(obj).toEqual({a:1}) |
| .toBeTruthy() | Es un valor truthy | expect('hola').toBeTruthy() |
| .toBeFalsy() | Es un valor falsy | expect(0).toBeFalsy() |
| .toBeNull() | Es exactamente null | expect(x).toBeNull() |
| .toBeUndefined() | Es exactamente undefined | expect(x).toBeUndefined() |
| .toBeGreaterThan(n) | Mayor que un número | expect(10).toBeGreaterThan(5) |
| .toBeLessThan(n) | Menor que un número | expect(2).toBeLessThan(5) |
| .toContain(x) | Array o string contiene un valor | expect([1,2,3]).toContain(2) |
| .toHaveLength(n) | Array o string con longitud N | expect([1,2]).toHaveLength(2) |
| .toMatch(regex) | String matchea una regex | expect('hola').toMatch(/o/) |
| .toThrow(msg?) | Función lanza un error | expect(() => fn()).toThrow() |
| .not.toBe(x) | Negación de cualquier matcher | expect(2).not.toBe(3) |
⚡ .toBe vs .toEqual:
.toBe usa === (referencia).
Para objetos y arrays, dos referencias distintas con el mismo contenido NO son iguales con .toBe.
Usa .toEqual para comparar contenido de objetos/arrays.
8
Mocking: simular dependencias con vi
A veces tu código depende de cosas que no quieres ejecutar de verdad en un test: una API externa, una BD, el reloj del sistema.
Para eso existe el mocking: reemplazas la dependencia por una función falsa controlada por ti.
Vitest expone el namespace vi:
vi.fn() — crear una función espía
1import { describe, it, expect, vi } from 'vitest';2 3it('callback se llama con el resultado', () => {4 const callback = vi.fn(); // función mock5 procesar(10, callback);6 7 expect(callback).toHaveBeenCalled(); // ¿se llamo?8 expect(callback).toHaveBeenCalledWith(20); // ¿con qué argumentos?9 expect(callback).toHaveBeenCalledTimes(1); // ¿cuántas veces?10});
vi.mock() — mockear un módulo entero
1import { describe, it, expect, vi } from 'vitest';2import { obtenerUsuario } from './api.js';3import { mostrarBienvenida } from './ui.js';4 5// Reemplaza el módulo real por uno fake6vi.mock('./api.js', () => ({7 obtenerUsuario: vi.fn(() => Promise.resolve({ name: 'Ana' }))8}));9 10it('muestra el nombre del usuario', async () => {11 const resultado = await mostrarBienvenida(1);12 expect(resultado).toBe('Hola Ana');13});
vi.spyOn() — espiar sin reemplazar
1it('console.log se llama al hacer login', () => {2 const spy = vi.spyOn(console, 'log');3 4 login('ana@test.com');5 6 expect(spy).toHaveBeenCalledWith('Login exitoso');7 spy.mockRestore(); // limpiar8});
⚠️ Cuándo NO mockear:
Si mockeas TODO, tus tests pasan pero no garantizan nada real. Mockea solo lo que es lento, externo o no-determinista:
APIs HTTP, BDs, fechas, generadores de random. Lo demás, déjalo correr de verdad.
9
Configuración e integración con Astro
En Astro, Vitest funciona directamente. No necesitas configuración para empezar.
Si quieres customizar, crea vitest.config.ts en la raíz:
1import { defineConfig } from 'vitest/config';2 3export default defineConfig({4 test: {5 globals: true, // describe/it/expect sin imports6 environment: 'node', // o 'jsdom' si testeas DOM7 include: ['src/**/*.test.{js,ts}'],8 coverage: {9 provider: 'v8',10 reporter: ['text', 'html'],11 exclude: ['node_modules', 'dist'],12 }13 }14});
¿Necesitas testear el DOM?
Si tu código manipula document o window,
configura environment: 'jsdom' e instala:
1npm install -D jsdom
10
Buenas prácticas
✓ Un test, una afirmación clara
Cada it debe probar UNA cosa. Si el nombre tiene "y", probablemente son dos tests.
✓ Nombres descriptivos
"devuelve null si el usuario no existe" mejor que "test 3". El nombre del test es documentación.
✓ Patrón AAA: Arrange, Act, Assert
Estructura cada test en 3 bloques: prepara datos, ejecuta la acción, verifica el resultado.
✓ Tests independientes
El orden de ejecución no debe importar. No dependas del estado dejado por otro test.
No persigas el 100% de coverage
El 80% de coverage en lo importante > 100% testeando getters tontos. Cobertura es una métrica, no un objetivo.
No testees implementación, testea comportamiento
Si refactorizas la función pero el comportamiento sigue igual, los tests deberían seguir pasando.
11
Resumen
- •Testing = código que verifica que tu código funciona, automáticamente.
- •Tipos por nivel: unit (rápidos, muchos), integration (medios), E2E (lentos, pocos).
- •Tipos por conocimiento: caja negra (solo input/output), caja blanca (ves el código).
- •Vitest es el test runner moderno para Vite/Astro: rápido, ESM, API tipo Jest.
- •Estructura:
describe(...) + it(...) + expect(x).toBe(y). - •Mockea solo lo externo, lento o no-determinista. Lo demás, déjalo correr.