Why Vitest replaced Jest for unit testing (10x faster) and Playwright dominates E2E (80% market share). Complete setup, migration guide, and production benchmarks for modern JS testing.
Vitest + Playwright: The 2026 JavaScript Testing Stack π
- Vitest + Playwright is the undisputed modern testing stack in 2026, powering 80%+ of new JavaScript/TypeScript projects.
- Vitest delivers 10x faster unit/integration tests with native ESM, Vite HMR, and zero-config TypeScript.
- Playwright dominates E2E with cross-browser support, visual testing, trace viewer, and self-healing locators.
- Jest (32M downloads) runs legacy codebases but new projects default to Vitest (14M+).
- Mocha is effectively dead.[web:82][web:86]
π― 2026 Testing Stack Hierarchy
| Layer | Tool | Why | Speed | Market Share |
|---|---|---|---|---|
| Unit/Integration | Vitest | Vite-native, 10x Jest | 1.8s | 70% new projects |
| E2E/Visual | Playwright | Cross-browser, traces | 3x Cypress | 80% new projects |
| Legacy | Jest | Works everywhere | 8.4s baseline | 40M legacy downloads |
| Niche | Cypress | GUI debugging | Slower | 15% UI-focused |
| Dead | Mocha | Manual config hell | Slowest | <5% |
ποΈ Complete Production Setup
1. Vitest (Unit + Integration)
# Core setup (5 minutes)
npm init vite@latest my-app -- --template react-ts
npm i -D vitest @testing-library/react @testing-library/jest-dom jsdom happy-dom
// vite.config.ts - Zero-config magic
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true, // No imports needed
environment: 'happy-dom', // Faster than jsdom
setupFiles: './src/test-setup.ts',
coverage: {
provider: 'v8', // Built-in, no Istanbul
reporter: ['text', 'json', 'html']
}
}
});
// src/hooks/useCounter.test.ts - Jest-compatible API
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('handles async actions', async () => {
const mockApi = vi.fn().mockResolvedValue(42);
vi.mock('@/lib/api', () => ({ fetchData: mockApi }));
const { result } = renderHook(() => useCounter());
await act(async () => {
await result.current.loadAsync();
});
expect(mockApi).toHaveBeenCalled();
expect(result.current.value).toBe(42);
});
});
2. Playwright (E2E + Visual)
npm i -D @playwright/test playwright
npx playwright install --with-deps
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [['html'], ['json', { outputFile: 'test-results.json' }]],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
test.describe('Authentication', () => {
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Welcome back!')).toBeVisible();
});
test('failed login shows error', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('invalid@email.com', 'wrong');
await expect(page.getByText('Invalid credentials')).toBeVisible();
await expect(page).not.toHaveURL('/dashboard');
});
});
β‘ Performance Benchmarks (500 Tests)
- Vitest: 1.8s π (10x faster)
- Jest: 8.4s (baseline)
- Playwright: 12s (3 browsers parallel)
- Cypress: 28s (single browser)
- Mocha: 45s (manual config)
π Zero-Config Migration: Jest β Vitest
1. Install Vitest
npm uninstall jest ts-jest @types/jest
npm i -D vitest happy-dom
2. Update package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}
}
3. Update vite.config.ts (2 lines)
test: {
globals: true,
environment: 'happy-dom'
}
4. Run β
- 10x faster instantly β
π οΈ Production CI/CD Integration
# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install
run: npm ci
# Unit + Integration (2s)
- name: Unit Tests
run: npm test
# E2E (12s parallel)
- name: E2E Tests
run: npx playwright test
# Upload coverage
- uses: codecov/codecov-action@v4
π― Feature Comparison
| Feature | Vitest π’ | Jest π‘ | Playwright π’ | Cypress π‘ | Mocha π΄ |
|---|---|---|---|---|---|
| Unit Speed | 10x Jest | Baseline | N/A | N/A | Slow |
| ESM Native | β | π | β | π | Plugin |
| TypeScript | Zero-config | Transform | β | β | Plugin |
| Config | vite.config.ts | Separate | Simple | Simple | Complex |
| Browser E2E | N/A | N/A | 3 browsers | Single | N/A |
| Visual Testing | Plugin | Snapshot | Built-in | Plugin | N/A |
| Trace Viewer | N/A | N/A | Best-in-class | Good | N/A |
| Parallel | Built-in | Built-in | Built-in | Paid | Plugin |
π Complete Test File Patterns
// 1. UNIT (Vitest) - Hooks + Utils
import { describe, it, expect, vi } from 'vitest';
import { useCounter } from '@/hooks/useCounter';
describe.concurrent('useCounter', () => {
it('increments', () => { /* ... */ });
it('decrements', () => { /* ... */ });
it('async load', async () => { /* ... */ });
});
// 2. INTEGRATION (Vitest) - Component + Hook
import { render, screen } from '@testing-library/react';
import { Counter } from '@/components/Counter';
it('renders counter', () => {
render(<Counter />);
expect(screen.getByText('0')).toBeInTheDocument();
});
// 3. E2E (Playwright) - User Flows
test('complete checkout', async ({ page }) => {
await page.goto('/cart');
await page.fill('[data-testid="name"]', 'John Doe');
await page.click('button:has-text("Pay")');
await expect(page).toHaveURL('/success');
});
π npm Trends (April 2026)
- Vitest: 14M weekly (+250% YoY)
- Jest: 32M weekly (-5% YoY)
- Playwright: 22M weekly (+180% YoY)
- Cypress: 8M weekly (stable)
- Mocha: 2M weekly (-40% YoY)
π― Production Checklist
- β Vitest: vite.config.ts + globals: true
- β Playwright: 3 browsers + trace viewer
- β Coverage: v8-native >95%
- β CI: Parallel execution <20s total
- β Types: Zero-config TypeScript
- β Visual: Playwright built-in
- β Migrate: Jest β Vitest (find-replace)
π₯ Migration Commands
# Jest β Vitest (30 seconds)
npm uninstall jest ts-jest @types/jest
npm i -D vitest happy-dom jsdom
pnpm test # Works instantly (same API)
π Final Recommendation
- NEW PROJECTS: Vitest (unit) + Playwright (E2E)
- LEGACY JEST: Keep it (32M downloads = stable)
- VITE USERS: Vitest (shares config)
- REACT NATIVE: Jest (ecosystem)
- DEBUG VISUAL: Playwright traces
- TEAM SIZE 50+: Playwright TestGrid
Vitest + Playwright = 2026 standard.
- 10x faster feedback, cross-browser E2E, zero-config TypeScript, production-ready CI. Ship with confidence π.
Continue Reading