Volver al inicio
Anexo: Testing con Vitest

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.

Input
suma(2, 3)
Funcion suma()
¿Que hace por dentro?
Output
5
¿Que tests escribes? Solo casos de uso:suma(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
Integration
E2E
Unit
ms
$
Integration
s
$$
E2E
min
$$$
Unit: Funciones puras, una unidad aislada

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.

RED
Test falla
GREEN
Test pasa
REFACTOR
Mejora el codigo
↻ y vuelta a empezar
Fase actual: RED
Escribes un test para una funcion que aun no existe. Falla.

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.

1

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}
2

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});
3

Ejecutar los tests

1npm test

Verás algo así en la terminal:

1 ✓ src/utils/calculadora.test.js (3)
2 ✓ suma
3 ✓ devuelve la suma de dos numeros positivos
4 ✓ funciona con numeros negativos
5 ✓ lanza un error si recibe un string
6
7 Test Files 1 passed (1)
8 Tests 3 passed (3)
9 Duration 142ms
4

Que 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 positivos
2 AssertionError: expected 5 to be 99
3 - Expected: 99
4 + Received: 5
5
6 ❯ src/utils/calculadora.test.js:5:24

💡 Resumen del flujo:

  1. Crea tu función en archivo.js
  2. Crea tests en archivo.test.js
  3. npm test en 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 igualdad
2expect({a: 1}).toEqual({a: 1}); // igualdad profunda
3expect([1, 2, 3]).toContain(2); // contenido
4expect(() => fn()).toThrow(); // lanza error

7 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 primitivosexpect(2+2).toBe(4)
.toEqual(x)Igualdad profunda para objetos y arraysexpect(obj).toEqual({a:1})
.toBeTruthy()Es un valor truthyexpect('hola').toBeTruthy()
.toBeFalsy()Es un valor falsyexpect(0).toBeFalsy()
.toBeNull()Es exactamente nullexpect(x).toBeNull()
.toBeUndefined()Es exactamente undefinedexpect(x).toBeUndefined()
.toBeGreaterThan(n)Mayor que un númeroexpect(10).toBeGreaterThan(5)
.toBeLessThan(n)Menor que un númeroexpect(2).toBeLessThan(5)
.toContain(x)Array o string contiene un valorexpect([1,2,3]).toContain(2)
.toHaveLength(n)Array o string con longitud Nexpect([1,2]).toHaveLength(2)
.toMatch(regex)String matchea una regexexpect('hola').toMatch(/o/)
.toThrow(msg?)Función lanza un errorexpect(() => fn()).toThrow()
.not.toBe(x)Negación de cualquier matcherexpect(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 mock
5 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 fake
6vi.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(); // limpiar
8});

⚠️ 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 imports
6 environment: 'node', // o 'jsdom' si testeas DOM
7 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.