Jest był standardem od 2016. Napisałem w nim tysiące testów. Był dobry na swój czas. W 2026 nie jest dobry na dziś.

Vitest to jego naturalny następca. API prawie identyczne (możesz migrować w godzinę), ale silnik pod spodem różny. Vite zamiast Babela. Native ESM zamiast transformacji CommonJS. Watch mode, który reaguje w 100 ms zamiast 3 sekund.

Rok temu zmigrowałem pierwszy projekt z Jest na Vitest. Miesiąc później zmigrowałem wszystkie. Dziś każdy nowy projekt w agencji startuje na Vitest.

Ten post pokaże Ci konkretne różnice, jak wygląda migracja i dlaczego w 2026 nie ma sensu zaczynać nowego projektu na Jest.

Krótka odpowiedź

Nowe projekty: Vitest bezdyskusyjnie.

Powody, w kolejności ważności:

  • 5-10x szybszy watch mode
  • Native TypeScript i ESM support (bez konfiguracji)
  • Kompatybilne API z Jest (bariera migracji zero)
  • Lepsze error messages
  • Vite jako silnik (świetna integracja z resztą stacka)
  • Aktywne rozwój, backing Vite team (Vue.js)

Istniejące projekty na Jest: migruj, kiedy boli. Migracja jest łatwa, ale bez business value samo dla siebie.

Co to jest Vitest

Vitest to test framework napisany na bazie Vite. Vite (bundler od Evan You, twórcy Vue) zastępuje Webpack w wielu współczesnych stackach.

Vitest wykorzystuje Vite jako transformator kodu. Rezultat: dramatycznie szybszy test runner z mniej ceremonii.

API jest kompatybilne z Jest w 95% przypadków. Twoje describe, it, expect, beforeEach działają identycznie.

Dlaczego Jest zaczyna boleć

Jest był świetny w 2016. W 2026 pokazuje wiek.

Slow watch mode. Zmieniasz jeden plik, czekasz 3-5 sekund na rerun. W Vitest 100-500 ms.

Config hell. Jest z TypeScript wymaga babel-jest, ts-jest, prezetów, moduleNameMapper. Vitest: zero config dla TS.

No ESM. Jest do niedawna słabo supportuje ESM. Musisz --experimental-vm-modules albo transform. Vitest: native ESM.

Slow startup. Jest cold start: 5-10 sekund. Vitest: 500 ms - 1 sekunda.

Wolniejsze coverage. Jest coverage z c8/nyc jest wolne. Vitest ma vite-native coverage, dramatycznie szybsze.

Kruchość z współczesnymi stackami. Vite, TSX, monorepo, ESM libraries. Jest ma z każdym problemy. Vitest natywnie działa.

Performance: realne liczby

Konkretny projekt: monorepo Next.js + Strapi, 800 unit testów.

Jest:

  • Cold run: 24 sekundy
  • Watch mode reaction: 3.2 sekundy
  • Coverage run: 42 sekundy
  • Cache miss (nowa dependency): +8 sekund

Vitest:

  • Cold run: 8 sekund
  • Watch mode reaction: 350 ms
  • Coverage run: 12 sekund
  • Cache miss: +2 sekundy

3x szybciej cold run. 10x szybciej watch mode. Watch mode to gdzie żyjesz podczas developmentu. 10x różnica to gigantyczna oszczędność czasu.

Setup Vitest

W nowym projekcie:

pnpm add -D vitest @vitest/ui @vitest/coverage-v8

package.json:

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

Config w vitest.config.ts:

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      exclude: [
        'node_modules/',
        'tests/',
        '**/*.d.ts',
        '**/*.config.*',
      ],
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

Setup file (tests/setup.ts):

import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'

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

To wszystko. 5 minut setupu.

Pierwszy test

// src/utils/format.ts
export function formatPrice(cents: number): string {
  return `${(cents / 100).toFixed(2)} zł`
}

// src/utils/format.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice } from './format'

describe('formatPrice', () => {
  it('formats cents to zloty', () => {
    expect(formatPrice(1250)).toBe('12.50 zł')
  })

  it('handles zero', () => {
    expect(formatPrice(0)).toBe('0.00 zł')
  })

  it('handles negative amounts', () => {
    expect(formatPrice(-500)).toBe('-5.00 zł')
  })
})

Uruchamianie: pnpm test. Watch mode by default. Zmienisz kod, testy się przelecą. 100 ms.

Testowanie React komponentów

Vitest + React Testing Library:

import { render, screen } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { describe, it, expect } from 'vitest'
import { Counter } from './Counter'

describe('Counter', () => {
  it('renders initial count', () => {
    render(<Counter />)
    expect(screen.getByText('Count: 0')).toBeInTheDocument()
  })

  it('increments count on button click', async () => {
    const user = userEvent.setup()
    render(<Counter />)
    
    await user.click(screen.getByRole('button', { name: 'Increment' }))
    
    expect(screen.getByText('Count: 1')).toBeInTheDocument()
  })
})

Identyczne API jak Jest + RTL. Zero learning curve.

Mocking

Vitest ma własne mocking API:

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

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

describe('user page', () => {
  it('displays user name', async () => {
    vi.mocked(fetchUser).mockResolvedValue({ name: 'Jan' })
    
    const user = await fetchUser('123')
    expect(user.name).toBe('Jan')
    expect(fetchUser).toHaveBeenCalledWith('123')
  })
})

vi w Vitest = jest w Jest. Almost identyczne API. Migration to search-and-replace.

Testowanie API endpointów (Next.js)

Server actions i API routes:

import { describe, it, expect } from 'vitest'
import { createUser } from './actions'

describe('createUser action', () => {
  it('creates user with valid data', async () => {
    const formData = new FormData()
    formData.append('email', '[email protected]')
    formData.append('name', 'Jan')
    
    const result = await createUser(formData)
    
    expect(result.success).toBe(true)
    expect(result.user?.email).toBe('[email protected]')
  })

  it('returns error for invalid email', async () => {
    const formData = new FormData()
    formData.append('email', 'invalid')
    formData.append('name', 'Jan')
    
    const result = await createUser(formData)
    
    expect(result.success).toBe(false)
    expect(result.error).toContain('email')
  })
})

Server-side kod testujesz jak każdą inną funkcję. Bez browser’a, szybko.

Snapshot testing

Kontrowersyjne, ale dostępne:

it('matches snapshot', () => {
  const result = renderToString(<UserCard user={mockUser} />)
  expect(result).toMatchSnapshot()
})

Snapshots idą do __snapshots__/ folderu. Update: pnpm test -u.

Osobiście używam sparingly. Snapshots dobre dla stabilnych komponentów, złe dla często się zmieniających. Alternative: explicit assertions.

Coverage

pnpm test:coverage

Generuje raport HTML w coverage/index.html. Pokazuje pokrycie plik po pliku, linia po linii.

Config coverage w vitest.config.ts:

coverage: {
  provider: 'v8',  // szybszy niż istanbul
  reporter: ['text', 'html', 'lcov'],
  thresholds: {
    lines: 80,
    functions: 80,
    branches: 75,
    statements: 80,
  },
  exclude: [
    'node_modules/',
    'tests/',
    '**/*.config.*',
    '**/*.d.ts',
  ],
}

thresholds fail build, jeśli coverage spada. Enforcuje discipline.

Uwaga: 100% coverage nie znaczy 100% correctness. Coverage to signal, nie cel.

UI Mode

Vitest ma --ui flag:

pnpm test:ui

Otwiera GUI w browserze:

  • Lista testów z filtrowaniem
  • Test details, console output
  • Time series wykonania
  • Coverage w GUI
  • Snapshot preview

Świetne do debugowania testów. Alternative dla Jest (Jest Preview, ale mniej dopracowane).

Watch mode magic

Vitest watch mode jest dramatycznie inteligentniejszy od Jest.

Reactive. Zmieniasz utils/format.ts = uruchamiają się tylko testy, które importują ten plik. Nie wszystkie testy.

Test filtering. W terminalu: p (filter files), t (filter test names), q (quit). Bez konfiguracji.

Zero config. Bez jest.config.js z watchPlugins.

W praktyce: zmieniasz kod, test się przelatuje w 200 ms, widzisz feedback natychmiast. Dev experience na innym poziomie niż Jest.

Migracja z Jest

Konkretny playbook.

Krok 1: install Vitest

pnpm add -D vitest @vitest/ui @vitest/coverage-v8
pnpm remove jest ts-jest babel-jest @types/jest

Krok 2: przemianuj config

jest.config.jsvitest.config.ts. Większość opcji identyczna, tylko struktura config różna.

Krok 3: update setup files

Jest globals (test, expect) → Vitest globals lub explicit imports.

W vitest.config.ts dodaj globals: true, jeśli nie chcesz zmieniać kodu testów.

Krok 4: replace jest → vi

Search-and-replace:

  • jest.mockvi.mock
  • jest.fnvi.fn
  • jest.spyOnvi.spyOn

Większość IDE robi to w 30 sekund.

Krok 5: update package.json

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

Krok 6: uruchom testy

pnpm test. 95% testów przejdzie. Reszta wymaga drobnych fixów (edge cases).

Cały playbook: 2-4 godziny pracy dla projektu z 200-500 testami.

Częste błędy przy migracji

Zapominanie o globals: true. Testy padają: describe is not defined. Fix: globals: true w config albo import { describe, it, expect } from 'vitest' na topie każdego pliku.

jest.mock zapomniany. Zostawiony w kodzie po search-and-replace. Vitest nie rozpoznaje.

Snapshots nie kompatybilne. Format snapshots trochę inny. Delete stare, wygeneruj nowe.

Custom transforms. Jeśli miałeś custom transform w Jest (Babel plugins), musisz przenieść do Vite config.

Test isolation. Vitest domyślnie izoluje testy per plik. Jest podobnie. Ale różne default zachowania dla globalnego state.

Timing. Vitest jest szybszy, race conditions ukryte w Jest mogą się ujawnić.

Vitest z Playwright

Dla full testing stack: Vitest dla unit/integration, Playwright dla E2E.

Nie mieszaj. Vitest do:

  • Unit testów funkcji
  • Component testów (React Testing Library)
  • Integration testów API endpointów
  • Testów utils, hooków, services

Playwright do:

  • User flow testów
  • Cross-browser
  • Real browser interactions
  • Testów wymagających network, DOM manipulation w real browser

Vitest jest 100x szybszy od Playwright dla unit testów. Playwright jest niezastąpiony dla E2E.

CI/CD z Vitest

W GitHub Actions:

- name: Run tests
  run: pnpm test:run

- name: Coverage
  run: pnpm test:coverage

- name: Upload coverage
  uses: codecov/codecov-action@v4
  with:
    files: ./coverage/lcov.info

vitest run (bez watch) dla CI. Szybciej, nie czeka na zmiany.

Codecov (albo Coveralls, Codacy) do trackowania coverage w PR-ach.

Alternative do Vitest w 2026

Bun test. Bun (JavaScript runtime) ma wbudowany test runner. Bardzo szybki. Wybór, jeśli używasz Bun globalnie.

Node –test. Node.js ma wbudowany test runner od v20. Minimalistyczny, ale rośnie w popularności. Zero dependencies.

Deno.test. Jeśli używasz Deno.

Jest (nadal). Dla legacy projektów, gdzie migracja nie ma business value.

Dla nowych projektów Vitest jest wyborem. Bun test jeśli używasz Bun. Reszta niche.

Częste błędy

Nie testowanie async correctly. expect na promise bez await. Vitest to złapie, ale bez await warning.

Zbyt dużo mocków. Mockujesz 5 modułów, żeby przetestować jedną funkcję. Prawdopodobnie ta funkcja robi za dużo. Refactor, potem test.

Testowanie implementacji, nie behavior. Test spec: “użytkownik komponentu widzi X”. Nie “komponent wywołuje setState 3 razy”.

Brak izolacji. Testy współdzielą global state, kolejność matter. Vitest domyślnie izoluje, ale musisz być świadomy.

Coverage jako cel. “Musimy mieć 100% coverage!” Bad. Coverage to signal, jakość testów matter więcej.

Ignorowanie flakiness. Test raz zdaje, raz nie. retry: 2. Nie. Fix flakiness, nie ukryj.

Co czytać dalej

  • Vitest docs. Świetne, aktywnie rozwijane.
  • Testing Library docs. Best practices dla component tests.
  • Kent C. Dodds blog. Guru testowania w JS/React ecosystem.

Nie kupuj kursów. Docs + one project = wystarczy.

Od czego zacząć

Jeśli używasz Jest i myślisz o migracji:

  1. Zainstaluj Vitest obok Jest (mogą koegzystować).
  2. Przemigruj jeden plik testowy. Zobacz różnicę.
  3. Migruj po plikach. Nie all-at-once.
  4. Po tygodniu usuń Jest.

Jeśli zaczynasz nowy projekt: Vitest od dnia 1. Zero pytań.

Testowanie to inwestycja. Vitest sprawia, że koszt tej inwestycji spada dramatycznie. Programiści piszą testy chętniej, kiedy watch mode reaguje w 200 ms. Więcej testów = mniej bugów = szczęśliwszy klient.

Napisałem osobno o Playwright dla E2E. Vitest plus Playwright to moja recommended stack testowania dla 2026. Fast, reliable, świetne DX.