Back to blog

Vitest in 2026: The New Standard for Modern JavaScript Testing

Hello HaWkers, if you are still using Jest in new projects, you are probably wasting time and performance. Vitest has established itself as the standard testing framework for the modern JavaScript ecosystem in 2026.

Let's understand why the change happened and how you can benefit.

Why Vitest Won

The Context of Change

// Timeline of the shift from Jest to Vitest

const testingTimeline = {
  2020: 'Jest is the undisputed standard',
  2022: 'Vitest 0.x emerges with Vite',
  2023: 'Vitest 1.0 - production ready',
  2024: {
    vitest: 'Massive adoption in new projects',
    jest: 'Still dominant in legacy'
  },
  2025: {
    vitest4: 'Vitest 4.0 stable',
    angular: 'Angular 21 adopts Vitest as default',
    breaking: 'Turning point'
  },
  2026: {
    status: 'Vitest is the new standard',
    jest: 'Maintained for legacy',
    newProjects: '80%+ use Vitest'
  }
};

Vitest Advantages

// Why Vitest surpassed Jest

const vitestAdvantages = {
  // 1. Performance
  performance: {
    speedup: '2-10x faster than Jest',
    why: 'Reuses Vite pipeline',
    watch: 'Instant watch mode',
    benchmark: `
      Medium project (500 tests):
      Jest: 45 seconds
      Vitest: 8 seconds
    `
  },

  // 2. Zero configuration with Vite
  zeroConfig: {
    if: 'You use Vite',
    then: 'Vitest works without config',
    shared: 'Same plugin config, aliases, etc',
    dx: 'No need to maintain two configs'
  },

  // 3. Native ESM
  esm: {
    jest: 'ESM is an add-on, frequently breaks',
    vitest: 'ESM is native, works perfectly',
    impact: 'Can test ESM-only code without hacks'
  },

  // 4. Native TypeScript
  typescript: {
    jest: 'Requires ts-jest or babel',
    vitest: 'TypeScript works out of the box',
    types: 'Types included, no @types/vitest needed'
  },

  // 5. Jest-compatible API
  compatibility: {
    api: 'describe, it, expect - same API',
    migration: 'Most tests work without changes',
    mocks: 'vi.mock() similar to jest.mock()'
  },

  // 6. Modern features
  modernFeatures: {
    browserMode: 'Test in real browser (Playwright)',
    snapshotInline: 'Inline snapshots in code',
    coverage: 'Native v8 coverage',
    ui: 'Built-in interactive UI'
  }
};

Getting Started with Vitest

Basic Setup

// Installation and configuration

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

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

// 3. Configuration (optional - works without config with Vite)
// vitest.config.ts
import { defineConfig } from 'vitest/config';

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

    // Test environment
    environment: 'jsdom', // or 'node', 'happy-dom'

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

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

    // Include/exclude files
    include: ['**/*.{test,spec}.{ts,tsx}'],
  },
});

Writing Tests

// Basic syntax - very similar to 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);
  });
});

// With globals: true in config, you can omit imports
describe('Math functions', () => {
  it('should sum two numbers', () => {
    expect(sum(1, 2)).toBe(3);
  });
});

Mocking

// Mocking in Vitest

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

// Mock entire module
vi.mock('./api', () => ({
  fetchUser: vi.fn()
}));

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

  it('should fetch user by id', async () => {
    // Configure 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 specific function
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue(asyncValue);
mockFn.mockImplementation((x) => x * 2);

// Spy on existing method
const spy = vi.spyOn(object, 'method');
spy.mockReturnValue('mocked');

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

Testing Components

// Testing React components with 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 for Testing Library (tests/setup.ts)
import '@testing-library/jest-dom/vitest';

Testing Vue

// Testing Vue components with 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');
  });
});

Advanced Features

Browser Mode

// Test in real browser with Playwright

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

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

// Tests run in real browser!
// Useful for:
// - Canvas, WebGL
// - Specific Web APIs
// - 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 for visual regression
    await expect(page.locator('.chart')).toMatchScreenshot();
  });
});

Inline Snapshots

// Inline snapshots - more convenient than files

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

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

    // Inline snapshot - updates in the file itself!
    expect(formatUser(user)).toMatchInlineSnapshot(`
      {
        "displayName": "John (30 years old)",
        "initials": "J"
      }
    `);
  });
});

// When you run vitest -u, the snapshot is updated
// directly in the test file

Type Testing

// Test TypeScript types

// types.test-d.ts (note the -d in the name)
import { describe, it, expectTypeOf } from 'vitest';
import { createStore } from './store';

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

    // Test if type is correct
    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

// Visual interface for tests

// Run: npx vitest --ui

// UI features:
// - View all tests
// - Filter by status (passed, failed, skipped)
// - See test code
// - See console output
// - Re-run specific tests
// - Module graph visualization

// Excellent for:
// - Debugging failing tests
// - Understanding coverage
// - Onboarding new devs

Migrating from Jest to Vitest

Migration Guide

// Steps to migrate

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

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

    export default defineConfig({
      test: {
        globals: true, // For Jest compatibility
        environment: 'jsdom',
      },
    });
  `,

  // 3. Update 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. Update imports (if not using globals)
  step5: `
    // Before
    import { jest } from '@jest/globals';

    // After
    import { vi } from 'vitest';
  `
};

API Differences

// Differences between Jest and Vitest

const apiDifferences = {
  // Mocking
  mocking: {
    jest: 'jest.fn(), jest.mock(), jest.spyOn()',
    vitest: 'vi.fn(), vi.mock(), vi.spyOn()',
    note: 'Only different prefix'
  },

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

  // Module mocking
  moduleMock: {
    jest: `
      jest.mock('./module', () => ({
        foo: jest.fn()
      }));
    `,
    vitest: `
      vi.mock('./module', () => ({
        foo: vi.fn()
      }));
    `,
    vitestAlternative: `
      // Vitest also supports:
      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 has toMatchInlineSnapshot()'
  },

  // Globals
  globals: {
    jest: 'Globals by default',
    vitest: 'Needs globals: true or explicit imports',
    recommendation: 'Use explicit imports for type safety'
  }
};

Automatic Codemod

// Use codemod for automated migration

// Install
// npx @vitest/codemod jest

// The codemod does:
// - Renames jest.* to vi.*
// - Updates imports
// - Adjusts configuration

// Afterward, manually review:
// - Complex mocks
// - Setup files
// - Custom matchers

Best Practices with Vitest

Test Organization

// Recommended structure

const testStructure = {
  // Option 1: Co-located (recommended)
  colocated: `
    src/
      components/
        Button/
          Button.tsx
          Button.test.tsx    ← Next to the code
          Button.stories.tsx
      utils/
        format.ts
        format.test.ts       ← Next to the code
  `,

  // Option 2: Separate folder
  separate: `
    src/
      components/
        Button.tsx
    tests/
      components/
        Button.test.tsx      ← Separate folder
  `,

  recommendation: `
    Co-located is better:
    - Easy to find tests
    - Refactor together
    - Encourages writing tests
  `
};

Useful Patterns

// Patterns that work well

// 1. Factory functions for 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. Organized describe blocks
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', () => {});
  });
});

Conclusion

Vitest has become the standard for JavaScript testing for good reasons: it is faster, more modern, and has better DX than Jest. Migration is simple and the benefits are immediate.

When to use Vitest:

  • New projects (always)
  • Projects with Vite (easy migration)
  • Projects that need ESM
  • When test performance matters

When to keep Jest:

  • Legacy projects working fine
  • Migration cost does not pay off
  • Dependencies that require Jest

Recommended actions:

  1. Use Vitest in new projects
  2. Plan migration for active projects
  3. Explore browser mode for visual tests
  4. Use UI mode for debugging

To learn more about the modern JavaScript ecosystem, read: VoidZero 2026.

Let's go! 🦅

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments