Volver al blog

Vitest en 2026: El Nuevo Estándar de Pruebas en JavaScript Moderno

Hola HaWkers, si todavía estás usando Jest en nuevos proyectos, probablemente estás perdiendo tiempo y rendimiento. Vitest se ha consolidado como el framework de pruebas estándar del ecosistema JavaScript moderno en 2026.

Vamos a entender por qué ocurrió el cambio y cómo puedes beneficiarte.

Por Qué Vitest Ganó

El Contexto del Cambio

// Timeline del cambio de Jest a Vitest

const testingTimeline = {
  2020: 'Jest es el estándar indiscutible',
  2022: 'Vitest 0.x surge con Vite',
  2023: 'Vitest 1.0 - listo para producción',
  2024: {
    vitest: 'Adopción masiva en nuevos proyectos',
    jest: 'Todavía dominante en legacy'
  },
  2025: {
    vitest4: 'Vitest 4.0 estable',
    angular: 'Angular 21 adopta Vitest como estándar',
    breaking: 'Punto de inflexión'
  },
  2026: {
    status: 'Vitest es el nuevo estándar',
    jest: 'Mantenido para legacy',
    newProjects: '80%+ usan Vitest'
  }
};

Ventajas de Vitest

// Por qué Vitest superó a Jest

const vitestAdvantages = {
  // 1. Rendimiento
  performance: {
    speedup: '2-10x más rápido que Jest',
    why: 'Reutiliza el pipeline de Vite',
    watch: 'Watch mode instantáneo',
    benchmark: `
      Proyecto mediano (500 tests):
      Jest: 45 segundos
      Vitest: 8 segundos
    `
  },

  // 2. Configuración cero con Vite
  zeroConfig: {
    if: 'Usas Vite',
    then: 'Vitest funciona sin config',
    shared: 'Misma config de plugins, aliases, etc',
    dx: 'No necesitas mantener dos configs'
  },

  // 3. ESM nativo
  esm: {
    jest: 'ESM es un add-on, frecuentemente falla',
    vitest: 'ESM es nativo, funciona perfectamente',
    impact: 'Puedes testear código ESM-only sin hacks'
  },

  // 4. TypeScript nativo
  typescript: {
    jest: 'Requiere ts-jest o babel',
    vitest: 'TypeScript funciona out of the box',
    types: 'Types incluidos, sin @types/vitest'
  },

  // 5. API compatible con Jest
  compatibility: {
    api: 'describe, it, expect - misma API',
    migration: 'La mayoría de los tests funcionan sin cambios',
    mocks: 'vi.mock() similar a jest.mock()'
  },

  // 6. Features modernas
  modernFeatures: {
    browserMode: 'Testea en browser real (Playwright)',
    snapshotInline: 'Snapshots inline en el código',
    coverage: 'v8 coverage nativo',
    ui: 'UI interactiva built-in'
  }
};

Comenzando con Vitest

Setup Básico

// Instalación y configuración

// 1. Instalar
// npm install -D vitest

// 2. Agregar script en package.json
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

// 3. Configuración (opcional - funciona sin config con Vite)
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // Globals (describe, it, expect) sin import
    globals: true,

    // Ambiente de prueba
    environment: 'jsdom', // o 'node', 'happy-dom'

    // Setup files
    setupFiles: ['./tests/setup.ts'],

    // Coverage
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      exclude: ['node_modules/', 'tests/']
    },

    // Incluir/excluir archivos
    include: ['**/*.{test,spec}.{ts,tsx}'],
  },
});

Escribiendo Tests

// Sintaxis básica - muy similar a Jest

// math.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { sum, multiply } from './math';

describe('Math functions', () => {
  it('should sum two numbers', () => {
    expect(sum(1, 2)).toBe(3);
  });

  it('should multiply two numbers', () => {
    expect(multiply(3, 4)).toBe(12);
  });
});

// Con globals: true en config, puedes omitir imports
describe('Math functions', () => {
  it('should sum two numbers', () => {
    expect(sum(1, 2)).toBe(3);
  });
});

Mocking

// Mocking en Vitest

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fetchUser } from './api';
import { UserService } from './UserService';

// Mock de módulo entero
vi.mock('./api', () => ({
  fetchUser: vi.fn()
}));

describe('UserService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should fetch user by id', async () => {
    // Configurar mock
    vi.mocked(fetchUser).mockResolvedValue({
      id: 1,
      name: 'John'
    });

    const service = new UserService();
    const user = await service.getUser(1);

    expect(fetchUser).toHaveBeenCalledWith(1);
    expect(user.name).toBe('John');
  });
});

// Mock de función específica
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue(asyncValue);
mockFn.mockImplementation((x) => x * 2);

// Spy en método existente
const spy = vi.spyOn(object, 'method');
spy.mockReturnValue('mocked');

// Mock de timers
vi.useFakeTimers();
vi.advanceTimersByTime(1000);
vi.runAllTimers();
vi.useRealTimers();

Testeando Componentes

// Testeando componentes React con Vitest + Testing Library

// Button.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('should render with text', () => {
    render(<Button>Click me</Button>);

    expect(screen.getByRole('button')).toHaveTextContent('Click me');
  });

  it('should call onClick when clicked', async () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click</Button>);

    await fireEvent.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledOnce();
  });

  it('should be disabled when loading', () => {
    render(<Button loading>Submit</Button>);

    expect(screen.getByRole('button')).toBeDisabled();
  });
});

// Setup para Testing Library (tests/setup.ts)
import '@testing-library/jest-dom/vitest';

Testeando Vue

// Testeando componentes Vue con Vitest

// Counter.test.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';

describe('Counter', () => {
  it('should render initial count', () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 5 }
    });

    expect(wrapper.text()).toContain('5');
  });

  it('should increment when button clicked', async () => {
    const wrapper = mount(Counter);

    await wrapper.find('button').trigger('click');

    expect(wrapper.text()).toContain('1');
  });
});

Features Avanzadas

Browser Mode

// Testear en browser real con Playwright

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      name: 'chromium', // o 'firefox', 'webkit'
      provider: 'playwright',
    },
  },
});

// ¡Los tests corren en browser real!
// Útil para:
// - Canvas, WebGL
// - Web APIs específicas
// - Visual regression

// visual.test.ts
import { describe, it, expect } from 'vitest';
import { page } from '@vitest/browser/context';

describe('Visual tests', () => {
  it('should render chart correctly', async () => {
    await page.goto('/chart');

    // Screenshot para visual regression
    await expect(page.locator('.chart')).toMatchScreenshot();
  });
});

Inline Snapshots

// Snapshots inline - más conveniente que archivos

import { describe, it, expect } from 'vitest';
import { formatUser } from './formatUser';

describe('formatUser', () => {
  it('should format user object', () => {
    const user = { name: 'John', age: 30 };

    // Snapshot inline - ¡se actualiza en el propio archivo!
    expect(formatUser(user)).toMatchInlineSnapshot(`
      {
        "displayName": "John (30 years old)",
        "initials": "J"
      }
    `);
  });
});

// Cuando corres vitest -u, el snapshot se actualiza
// directamente en el archivo de test

Type Testing

// Testear tipos TypeScript

// types.test-d.ts (nota el -d en el nombre)
import { describe, it, expectTypeOf } from 'vitest';
import { createStore } from './store';

describe('Store types', () => {
  it('should infer state type correctly', () => {
    const store = createStore({ count: 0 });

    // Testea si el tipo está correcto
    expectTypeOf(store.state.count).toBeNumber();
    expectTypeOf(store.state.count).not.toBeString();
  });

  it('should accept correct action types', () => {
    type Action = { type: 'increment' } | { type: 'decrement' };

    expectTypeOf<Action>().toMatchTypeOf<{ type: string }>();
  });
});

// vitest.config.ts
export default defineConfig({
  test: {
    typecheck: {
      enabled: true,
    },
  },
});

UI Mode

// Interfaz visual para tests

// Ejecuta: npx vitest --ui

// Features de la UI:
// - Visualizar todos los tests
// - Filtrar por status (passed, failed, skipped)
// - Ver código del test
// - Ver output de consola
// - Re-run tests específicos
// - Module graph visualization

// Excelente para:
// - Debug de tests fallando
// - Entender cobertura
// - Onboarding de nuevos devs

Migrando de Jest a Vitest

Guía de Migración

// Pasos para migrar

const migrationGuide = {
  // 1. Instalar Vitest
  step1: `
    npm remove jest @types/jest ts-jest babel-jest
    npm install -D vitest
  `,

  // 2. Crear config
  step2: `
    // vitest.config.ts
    import { defineConfig } from 'vitest/config';

    export default defineConfig({
      test: {
        globals: true, // Para compatibilidad con Jest
        environment: 'jsdom',
      },
    });
  `,

  // 3. Actualizar scripts
  step3: `
    // package.json
    {
      "scripts": {
        "test": "vitest",
        "test:run": "vitest run"
      }
    }
  `,

  // 4. Search and replace
  step4: {
    from: "jest.fn()",
    to: "vi.fn()",

    from2: "jest.mock(",
    to2: "vi.mock(",

    from3: "jest.spyOn(",
    to3: "vi.spyOn(",

    from4: "@jest/globals",
    to4: "vitest"
  },

  // 5. Actualizar imports (si no usas globals)
  step5: `
    // Antes
    import { jest } from '@jest/globals';

    // Después
    import { vi } from 'vitest';
  `
};

Diferencias de API

// Diferencias entre Jest y Vitest

const apiDifferences = {
  // Mocking
  mocking: {
    jest: 'jest.fn(), jest.mock(), jest.spyOn()',
    vitest: 'vi.fn(), vi.mock(), vi.spyOn()',
    note: 'Solo prefijo diferente'
  },

  // Timers
  timers: {
    jest: 'jest.useFakeTimers()',
    vitest: 'vi.useFakeTimers()',
    extra: 'Vitest tiene vi.advanceTimersByTimeAsync()'
  },

  // Module mocking
  moduleMock: {
    jest: `
      jest.mock('./module', () => ({
        foo: jest.fn()
      }));
    `,
    vitest: `
      vi.mock('./module', () => ({
        foo: vi.fn()
      }));
    `,
    vitestAlternative: `
      // Vitest también soporta:
      vi.mock('./module', async (importOriginal) => {
        const actual = await importOriginal();
        return {
          ...actual,
          foo: vi.fn()
        };
      });
    `
  },

  // Snapshot
  snapshot: {
    jest: 'expect(x).toMatchSnapshot()',
    vitest: 'expect(x).toMatchSnapshot()',
    extra: 'Vitest tiene toMatchInlineSnapshot()'
  },

  // Globals
  globals: {
    jest: 'Globals por defecto',
    vitest: 'Necesita globals: true o imports explícitos',
    recommendation: 'Usa imports explícitos para type safety'
  }
};

Codemod Automático

// Usa codemod para migración automatizada

// Instalar
// npx @vitest/codemod jest

// El codemod hace:
// - Renombra jest.* a vi.*
// - Actualiza imports
// - Ajusta configuración

// Después, revisa manualmente:
// - Mocks complejos
// - Setup files
// - Custom matchers

Buenas Prácticas con Vitest

Organización de Tests

// Estructura recomendada

const testStructure = {
  // Opción 1: Co-located (recomendado)
  colocated: `
    src/
      components/
        Button/
          Button.tsx
          Button.test.tsx    ← Junto al código
          Button.stories.tsx
      utils/
        format.ts
        format.test.ts       ← Junto al código
  `,

  // Opción 2: Carpeta separada
  separate: `
    src/
      components/
        Button.tsx
    tests/
      components/
        Button.test.tsx      ← Carpeta separada
  `,

  recommendation: `
    Co-located es mejor:
    - Fácil encontrar tests
    - Refactorizar junto
    - Incentiva escribir tests
  `
};

Patterns Útiles

// Patterns que funcionan bien

// 1. Factory functions para fixtures
function createUser(overrides = {}) {
  return {
    id: 1,
    name: 'John',
    email: 'john@example.com',
    ...overrides
  };
}

it('should update user name', () => {
  const user = createUser({ name: 'Jane' });
  // ...
});

// 2. Custom matchers
expect.extend({
  toBeValidEmail(received) {
    const pass = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(received);
    return {
      pass,
      message: () => `expected ${received} to be valid email`
    };
  }
});

// 3. Testing utilities
function renderWithProviders(ui, options = {}) {
  return render(ui, {
    wrapper: ({ children }) => (
      <QueryClientProvider client={queryClient}>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </QueryClientProvider>
    ),
    ...options
  });
}

// 4. Describe blocks organizados
describe('UserService', () => {
  describe('getUser', () => {
    it('should return user when found', () => {});
    it('should throw when not found', () => {});
  });

  describe('createUser', () => {
    it('should create user with valid data', () => {});
    it('should reject invalid email', () => {});
  });
});

Conclusión

Vitest se convirtió en el estándar de pruebas en JavaScript por buenas razones: es más rápido, más moderno, y tiene mejor DX que Jest. La migración es simple y los beneficios son inmediatos.

Cuándo usar Vitest:

  • Nuevos proyectos (siempre)
  • Proyectos con Vite (migración fácil)
  • Proyectos que necesitan ESM
  • Cuando el rendimiento de pruebas importa

Cuándo mantener Jest:

  • Proyectos legacy funcionando bien
  • Costo de migración no compensa
  • Dependencias que requieren Jest

Acciones recomendadas:

  1. Usa Vitest en nuevos proyectos
  2. Planea migración de proyectos activos
  3. Explora browser mode para pruebas visuales
  4. Usa UI mode para debug

Para entender más sobre el ecosistema JavaScript moderno, lee: VoidZero 2026.

¡Vamos con todo! 🦅

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios