Testing con Vitest

Intermedio
Vuejs
Vuejs
Actualizado: 27/03/2026

Por qué Vitest para proyectos Vue

Vitest es el test runner recomendado por el equipo de Vue para proyectos basados en Vite. Al compartir la misma configuración y pipeline de transformación que Vite, Vitest elimina la duplicación de configuraciones y ofrece una experiencia de desarrollo fluida.

Las principales ventajas de Vitest frente a otras alternativas son:

  • Nativo de Vite: reutiliza la configuración de vite.config.ts, incluyendo plugins, alias y transformaciones
  • API compatible con Jest: las funciones describe, it, expect y los mocks funcionan de la misma forma
  • Rendimiento superior: ejecución en paralelo con worker threads y transformación bajo demanda
  • Soporte nativo de ESM y TypeScript: sin necesidad de configuración adicional
  • Modo watch inteligente: solo re-ejecuta los tests afectados por los cambios

Instalación y configuración

Para instalar Vitest en un proyecto Vue existente:

npm install -D vitest

Vitest puede usar directamente el archivo vite.config.ts existente. Para definir opciones específicas de testing, se utiliza la propiedad test:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
  },
})

La opción globals: true permite usar describe, it y expect sin importarlos. La opción environment: 'jsdom' simula un entorno de navegador para tests que interactúan con el DOM.

Si se prefiere separar la configuración, se puede crear un archivo vitest.config.ts:

// vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'

export default mergeConfig(viteConfig, defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
  },
}))

Para que TypeScript reconozca los globales de Vitest, se añade al tsconfig.app.json:

{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}

Convenciones de archivos de test

Vitest detecta automáticamente archivos que coincidan con estos patrones:

  • **/*.test.ts o **/*.test.js
  • **/*.spec.ts o **/*.spec.js

Una organización común es colocar los tests junto al código fuente:

src/
  utils/
    formatters.ts
    formatters.test.ts
  services/
    api.ts
    api.test.ts

Estructura de un test: describe, it y expect

La estructura básica de un archivo de test agrupa los tests relacionados con describe y define cada caso con it o test:

import { describe, it, expect } from 'vitest'
import { calcularDescuento } from './pricing'

describe('calcularDescuento', () => {
  it('aplica un 10% de descuento para compras superiores a 100', () => {
    const resultado = calcularDescuento(200, 10)
    expect(resultado).toBe(180)
  })

  it('no aplica descuento si el porcentaje es 0', () => {
    const resultado = calcularDescuento(200, 0)
    expect(resultado).toBe(200)
  })

  it('lanza error si el porcentaje es negativo', () => {
    expect(() => calcularDescuento(200, -5)).toThrow('Porcentaje inválido')
  })
})

Matchers comunes

Vitest proporciona un conjunto de matchers para realizar aserciones:

// Igualdad
expect(2 + 2).toBe(4)                    // igualdad estricta (===)
expect({ a: 1 }).toEqual({ a: 1 })       // igualdad profunda de objetos

// Veracidad
expect(true).toBeTruthy()
expect(0).toBeFalsy()
expect(null).toBeNull()
expect(undefined).toBeUndefined()
expect('hello').toBeDefined()

// Números
expect(10).toBeGreaterThan(5)
expect(0.1 + 0.2).toBeCloseTo(0.3)

// Strings y arrays
expect('equipo').toContain('equi')
expect([1, 2, 3]).toContain(2)
expect([{ id: 1 }, { id: 2 }]).toContainEqual({ id: 1 })

// Excepciones
expect(() => { throw new Error('fallo') }).toThrow('fallo')

// Funciones mock
const fn = vi.fn()
fn('arg1')
expect(fn).toHaveBeenCalled()
expect(fn).toHaveBeenCalledWith('arg1')
expect(fn).toHaveBeenCalledTimes(1)

Setup y teardown

Para ejecutar lógica antes y después de los tests se utilizan las funciones de setup y teardown:

import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest'

describe('UserService', () => {
  let servicio: UserService

  beforeAll(() => {
    // Se ejecuta una vez antes de todos los tests del describe
    console.log('Iniciando suite de tests')
  })

  afterAll(() => {
    // Se ejecuta una vez después de todos los tests del describe
    console.log('Suite completada')
  })

  beforeEach(() => {
    // Se ejecuta antes de cada test individual
    servicio = new UserService()
  })

  afterEach(() => {
    // Se ejecuta después de cada test individual
    servicio.reset()
  })

  it('devuelve una lista de usuarios', () => {
    const usuarios = servicio.getAll()
    expect(usuarios).toEqual([])
  })
})

Mocking con vi

Vitest proporciona el objeto vi con utilidades para crear mocks, espías y reemplazar módulos:

vi.fn() - Funciones mock

import { describe, it, expect, vi } from 'vitest'

const callback = vi.fn()
callback('dato1')
callback('dato2')

expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenCalledWith('dato1')

// Mock con implementacion
const sumar = vi.fn((a: number, b: number) => a + b)
expect(sumar(2, 3)).toBe(5)

vi.spyOn() - Espiar métodos existentes

const carrito = {
  items: [] as string[],
  agregar(item: string) {
    this.items.push(item)
  }
}

const spy = vi.spyOn(carrito, 'agregar')
carrito.agregar('producto1')

expect(spy).toHaveBeenCalledWith('producto1')
expect(carrito.items).toContain('producto1')

spy.mockRestore()

vi.mock() - Reemplazar módulos completos

import { describe, it, expect, vi } from 'vitest'
import { fetchUsers } from './api'

vi.mock('./api', () => ({
  fetchUsers: vi.fn(() => Promise.resolve([
    { id: 1, name: 'Ana' },
    { id: 2, name: 'Carlos' }
  ]))
}))

describe('fetchUsers mock', () => {
  it('devuelve usuarios mockeados', async () => {
    const users = await fetchUsers()
    expect(users).toHaveLength(2)
    expect(users[0].name).toBe('Ana')
  })
})

Testing de código asíncrono

Los tests asíncronos se escriben con async/await:

import { describe, it, expect } from 'vitest'

async function obtenerDatos(id: number): Promise<{ id: number; nombre: string }> {
  const response = await fetch(`/api/items/${id}`)
  return response.json()
}

describe('obtenerDatos', () => {
  it('devuelve los datos del item', async () => {
    // Asumiendo que fetch está mockeado
    const datos = await obtenerDatos(1)
    expect(datos).toHaveProperty('id')
    expect(datos).toHaveProperty('nombre')
  })

  it('lanza error con id inválido', async () => {
    await expect(obtenerDatos(-1)).rejects.toThrow()
  })
})

Ejecución de tests

Vitest se ejecuta desde la terminal con diferentes modos:

# Modo watch (por defecto): re-ejecuta al detectar cambios
npx vitest

# Ejecución única sin watch
npx vitest run

# Ejecutar tests que coincidan con un patrón
npx vitest run formatters

# Modo watch explícito
npx vitest watch

Ejemplo de salida al ejecutar el script npm run test:unit en un proyecto Vue con Vitest configurado (puede mostrarse como vitest run según tu package.json):

Es recomendable añadir scripts en package.json:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

Cobertura de codigo

Para generar reportes de cobertura, se instala el proveedor:

npm install -D @vitest/coverage-v8

Y se configura en vite.config.ts:

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.test.ts', 'src/**/*.d.ts'],
    },
  },
})

Al ejecutar npx vitest run --coverage, se genera un reporte en la terminal y un directorio coverage/ con el reporte HTML detallado.

Ejemplo práctico completo

Un servicio de validación con su suite de tests:

// validators.ts
export function esEmailValido(email: string): boolean {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return regex.test(email)
}

export function esPasswordSeguro(password: string): boolean {
  if (password.length < 8) return false
  if (!/[A-Z]/.test(password)) return false
  if (!/[a-z]/.test(password)) return false
  if (!/[0-9]/.test(password)) return false
  return true
}
// validators.test.ts
import { describe, it, expect } from 'vitest'
import { esEmailValido, esPasswordSeguro } from './validators'

describe('esEmailValido', () => {
  it('acepta un email con formato correcto', () => {
    expect(esEmailValido('user@example.com')).toBe(true)
  })

  it('rechaza un email sin arroba', () => {
    expect(esEmailValido('userexample.com')).toBe(false)
  })

  it('rechaza un email sin dominio', () => {
    expect(esEmailValido('user@')).toBe(false)
  })

  it('rechaza una cadena vacía', () => {
    expect(esEmailValido('')).toBe(false)
  })
})

describe('esPasswordSeguro', () => {
  it('acepta un password que cumple todos los requisitos', () => {
    expect(esPasswordSeguro('MiClave123')).toBe(true)
  })

  it('rechaza un password menor a 8 caracteres', () => {
    expect(esPasswordSeguro('Ab1')).toBe(false)
  })

  it('rechaza un password sin mayusculas', () => {
    expect(esPasswordSeguro('miclave123')).toBe(false)
  })

  it('rechaza un password sin minúsculas', () => {
    expect(esPasswordSeguro('MICLAVE123')).toBe(false)
  })

  it('rechaza un password sin números', () => {
    expect(esPasswordSeguro('MiClaveSinNum')).toBe(false)
  })
})
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Vuejs

Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección