Menu
Back to Blog
1 min read
Web Scraping

Vitest: Next-Gen Testing mit Browser Mode

Vitest als Vite-native Testing Framework. Browser Mode, Component Testing, TypeScript-Support und Migration von Jest.

VitestTestingBrowser ModeComponent TestingPlaywrightReact Testing
Vitest: Next-Gen Testing mit Browser Mode

Vitest: Next-Gen Testing mit Browser Mode

Meta-Description: Vitest als Vite-native Testing Framework. Browser Mode, Component Testing, TypeScript-Support und Migration von Jest.

Keywords: Vitest, Testing, Browser Mode, Component Testing, Playwright, React Testing, TypeScript, Jest Alternative


Einführung

Vitest ist das native Testing-Framework für Vite und hat Jest in modernen Projekten weitgehend abgelöst. Mit Browser Mode testet Vitest Components in echten Browsern – ohne JSDOM-Limitierungen.


Warum Vitest?

┌─────────────────────────────────────────────────────────────┐
│                  VITEST vs JEST                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  VITEST                           JEST                      │
│  ────────────────────             ────────────────────      │
│  Vite-Native                      Babel-basiert             │
│  ESM First                        CommonJS Default          │
│  Shared Config mit Vite           Separate Config           │
│  Hot Module Replacement           Full Restart              │
│  Browser Mode                     JSDOM only                │
│                                                             │
│  Performance:                                               │
│  ├── Instant Watch Mode                                    │
│  ├── Native TypeScript Support                             │
│  ├── Out-of-Box ESM Support                                │
│  └── Shared Vite Pipeline                                  │
│                                                             │
│  Bundle: ~5MB vs ~65MB (Jest + Babel)                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Quick Setup

# Installation
npm install -D vitest

# Browser Mode (optional)
npm install -D @vitest/browser playwright

# UI (optional)
npm install -D @vitest/ui
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/test/setup.ts',
    include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov']
    }
  }
});
// src/test/setup.ts
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

afterEach(() => {
  cleanup();
});

Basic Unit Testing

// src/utils/math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

export async function fetchData(url: string): Promise<unknown> {
  const response = await fetch(url);
  return response.json();
}

// src/utils/math.test.ts
import { describe, it, expect, vi } from 'vitest';
import { add, multiply, fetchData } from './math';

describe('Math Utils', () => {
  it('should add two numbers', () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
  });

  it('should multiply two numbers', () => {
    expect(multiply(2, 3)).toBe(6);
    expect(multiply(0, 100)).toBe(0);
  });
});

describe('fetchData', () => {
  it('should fetch and parse JSON', async () => {
    const mockData = { name: 'Test' };

    // Mock fetch
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      json: () => Promise.resolve(mockData)
    }));

    const result = await fetchData('/api/test');
    expect(result).toEqual(mockData);

    vi.unstubAllGlobals();
  });
});

Component Testing (JSDOM)

// src/components/Counter.tsx
import { useState } from 'react';

interface CounterProps {
  initialValue?: number;
  onCountChange?: (count: number) => void;
}

export function Counter({ initialValue = 0, onCountChange }: CounterProps) {
  const [count, setCount] = useState(initialValue);

  const increment = () => {
    const newCount = count + 1;
    setCount(newCount);
    onCountChange?.(newCount);
  };

  const decrement = () => {
    const newCount = count - 1;
    setCount(newCount);
    onCountChange?.(newCount);
  };

  return (
    <div>
      <span data-testid="count">{count}</span>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  );
}

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

describe('Counter', () => {
  it('renders with initial value', () => {
    render(<Counter initialValue={5} />);
    expect(screen.getByTestId('count')).toHaveTextContent('5');
  });

  it('increments count when + clicked', () => {
    render(<Counter />);

    fireEvent.click(screen.getByText('+'));

    expect(screen.getByTestId('count')).toHaveTextContent('1');
  });

  it('decrements count when - clicked', () => {
    render(<Counter initialValue={5} />);

    fireEvent.click(screen.getByText('-'));

    expect(screen.getByTestId('count')).toHaveTextContent('4');
  });

  it('calls onCountChange callback', () => {
    const handleChange = vi.fn();
    render(<Counter onCountChange={handleChange} />);

    fireEvent.click(screen.getByText('+'));

    expect(handleChange).toHaveBeenCalledWith(1);
  });
});

Browser Mode (Real Browser Testing)

# Browser Mode initialisieren
npx vitest init browser
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: 'playwright',  // oder 'webdriverio'
      name: 'chromium',
      headless: true
    }
  }
});
// src/components/Dialog.browser.test.tsx
import { describe, it, expect } from 'vitest';
import { render } from 'vitest-browser-react';
import { Dialog } from './Dialog';

describe('Dialog (Browser Mode)', () => {
  it('should open and close correctly', async () => {
    const screen = render(
      <Dialog trigger={<button>Open</button>}>
        <p>Dialog Content</p>
      </Dialog>
    );

    // Initial: Dialog geschlossen
    await expect.element(screen.getByText('Dialog Content')).not.toBeVisible();

    // Öffnen
    await screen.getByText('Open').click();
    await expect.element(screen.getByText('Dialog Content')).toBeVisible();

    // Schließen mit Escape
    await screen.getByRole('dialog').press('Escape');
    await expect.element(screen.getByText('Dialog Content')).not.toBeVisible();
  });

  it('should focus trap correctly', async () => {
    const screen = render(
      <Dialog trigger={<button>Open</button>}>
        <input data-testid="input1" />
        <input data-testid="input2" />
        <button>Close</button>
      </Dialog>
    );

    await screen.getByText('Open').click();

    // Fokus sollte im Dialog gefangen sein
    const input1 = screen.getByTestId('input1');
    const input2 = screen.getByTestId('input2');

    await input1.focus();
    await input1.press('Tab');
    await expect.element(input2).toBeFocused();
  });
});

Mocking

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

// 1. Function Mocking
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue({ data: 'test' });
mockFn.mockImplementation((x) => x * 2);

// 2. Module Mocking
vi.mock('./api', () => ({
  fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test' }),
  updateUser: vi.fn().mockResolvedValue({ success: true })
}));

// 3. Partial Mocking
vi.mock('./utils', async (importOriginal) => {
  const actual = await importOriginal<typeof import('./utils')>();
  return {
    ...actual,
    complexFunction: vi.fn().mockReturnValue('mocked')
  };
});

// 4. Timer Mocking
describe('Timer Tests', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('should handle setTimeout', async () => {
    const callback = vi.fn();

    setTimeout(callback, 1000);

    expect(callback).not.toHaveBeenCalled();

    vi.advanceTimersByTime(1000);

    expect(callback).toHaveBeenCalledOnce();
  });

  it('should handle intervals', () => {
    const callback = vi.fn();

    setInterval(callback, 100);

    vi.advanceTimersByTime(350);

    expect(callback).toHaveBeenCalledTimes(3);
  });
});

// 5. Date Mocking
it('should mock current date', () => {
  const mockDate = new Date('2026-01-15');
  vi.setSystemTime(mockDate);

  expect(new Date().toISOString()).toContain('2026-01-15');

  vi.useRealTimers();
});

Snapshot Testing

import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { UserCard } from './UserCard';

describe('UserCard Snapshots', () => {
  it('matches snapshot for basic user', () => {
    const { container } = render(
      <UserCard
        user={{
          name: 'John Doe',
          email: 'john@example.com',
          avatar: '/avatar.jpg'
        }}
      />
    );

    expect(container).toMatchSnapshot();
  });

  it('matches inline snapshot', () => {
    const user = { name: 'Jane', role: 'admin' };

    expect(user).toMatchInlineSnapshot(`
      {
        "name": "Jane",
        "role": "admin",
      }
    `);
  });

  // File Snapshots
  it('matches file snapshot for large data', () => {
    const complexData = generateLargeDataset();

    expect(complexData).toMatchFileSnapshot('./snapshots/large-data.json');
  });
});

Test Coverage

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

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',  // oder 'istanbul'
      reporter: ['text', 'html', 'lcov', 'json'],
      reportsDirectory: './coverage',
      include: ['src/**/*.{ts,tsx}'],
      exclude: [
        'src/**/*.test.{ts,tsx}',
        'src/**/*.d.ts',
        'src/test/**/*'
      ],
      thresholds: {
        statements: 80,
        branches: 80,
        functions: 80,
        lines: 80
      }
    }
  }
});
# Coverage generieren
npx vitest --coverage

# Watch Mode mit Coverage
npx vitest --coverage --watch

Parallel & Sequential Tests

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

// Parallel (Default)
describe('Parallel Tests', () => {
  it.concurrent('test 1', async () => {
    await sleep(1000);
    expect(true).toBe(true);
  });

  it.concurrent('test 2', async () => {
    await sleep(1000);
    expect(true).toBe(true);
  });
  // Beide laufen parallel → ~1s total
});

// Sequential
describe('Sequential Tests', { sequential: true }, () => {
  let sharedState = 0;

  it('first', () => {
    sharedState = 1;
    expect(sharedState).toBe(1);
  });

  it('second', () => {
    sharedState = 2;
    expect(sharedState).toBe(2);
  });
});

// Test Isolation
describe.concurrent('Isolated Concurrent', () => {
  // Jeder Test bekommt eigene Isolation
  it('isolated 1', async ({ expect }) => {
    // ...
  });

  it('isolated 2', async ({ expect }) => {
    // ...
  });
});

API Testing

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

const server = setupServer(
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ]);
  }),

  http.post('/api/users', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: 3, ...body }, { status: 201 });
  })
);

beforeAll(() => server.listen());
afterAll(() => server.close());

describe('API Client', () => {
  it('fetches users', async () => {
    const response = await fetch('/api/users');
    const users = await response.json();

    expect(users).toHaveLength(2);
    expect(users[0].name).toBe('Alice');
  });

  it('creates a user', async () => {
    const response = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify({ name: 'Charlie' })
    });

    expect(response.status).toBe(201);

    const user = await response.json();
    expect(user.name).toBe('Charlie');
  });
});

Migration von Jest

// jest.config.js → vitest.config.ts

// Jest
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  }
};

// Vitest
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    setupFiles: './src/setupTests.ts',
    alias: {
      '@': './src'
    }
  }
});

// API-Änderungen
// Jest: jest.fn() → Vitest: vi.fn()
// Jest: jest.mock() → Vitest: vi.mock()
// Jest: jest.spyOn() → Vitest: vi.spyOn()

Fazit

Vitest bietet:

  1. Vite-Native: Shared Config, HMR, sofortiger Start
  2. Browser Mode: Echte Browser statt JSDOM
  3. TypeScript-First: Out-of-Box Support ohne Config
  4. Jest-kompatibel: Einfache Migration

Für jedes Vite-Projekt ist Vitest die natürliche Wahl.


Bildprompts

  1. "Test runner showing green checkmarks, development workflow visualization"
  2. "Browser and code split screen, component testing in real browser concept"
  3. "Fast forward icon with test results, instant feedback development loop"

Quellen