Upgrade Vitest 2 ke Vitest 5: Breaking Changes Sesuai Urutan Munculnya

8 menit baca.
Tags: Unit testJavaScriptTypeScript

Kalau kamu upgrade Vitest dari 2.x langsung ke 5, breaking changes dari 3.0, 4.0, dan 5.0 datang sekaligus. Sebagian menghentikan run, sebagian bikin seluruh file test gagal, dan beberapa diam-diam mengubah apa yang dijalankan tanpa mencetak apa pun. Berikut breaking changes Vitest 5 sesuai urutan kemunculannya, dari memindahkan sebuah test project (happy-dom, coverage-v8, dan UI) dari Vitest 2.1.9 ke 5.1.0. Semua fix di bawah sudah dicek di 5.1.0.

Jawaban cepat

Naikkan semua package Vitest bersamaan, lalu install ulang dari bersih:

{
  "devDependencies": {
    "vitest": "5.1.0",
    "@vitest/coverage-v8": "5.1.0",
    "@vitest/ui": "5.1.0",
    "vite": "^8.0.0"
  }
}
rm -rf node_modules package-lock.json
npm install
npx vitest run

Lalu perbaiki dengan urutan ini:

  1. Error config yang menghentikan startup: reporters: ['basic'], test.workspace, poolOptions.
  2. File yang gagal di-load: vi.mock atau vi.hoisted di dalam describe, dan option test di argumen ketiga.
  3. Test yang gagal: clearMocks sekarang default-nya aktif, dan .resolves yang tidak di-await sekarang bikin test gagal.
  4. Perubahan diam-diam: environmentMatchGlobs, vitest.workspace.ts, option coverage, dan pattern -t.

Install gagal dengan ERESOLVE

Upgrade vitest saja di atas install 2.x yang sudah ada akan gagal, karena @vitest/coverage-v8 dan @vitest/ui mengunci versi Vitest yang persis sebagai peer:

npm error code ERESOLVE
npm error ERESOLVE could not resolve
npm error Found: @vitest/coverage-v8@2.1.9
...
npm error Conflicting peer dependency: vitest@5.1.0
npm error   peer vitest@"5.1.0" from @vitest/coverage-v8@5.1.0

Samakan versi semua package @vitest/* di package.json, hapus node_modules dan lockfile, lalu install lagi.

Vitest 5 tidak lagi membawa Vite sendiri. Sekarang Vite jadi peer dependency (^6.4.0 || ^7.0.0 || ^8.0.0). npm otomatis meng-install Vite 8. Kalau pakai Yarn, tambahkan vite sendiri.

Field engines Vitest 5 bilang ^22.12.0 || ^24.0.0 || >=26.0.0. Suite-nya masih jalan di Node 20.20, tapi 20 di luar range yang didukung. Kalau CI atau hosting kamu build di Node 20, naikkan sekalian saat upgrade.

Error config yang menghentikan run

Run pertama di 5.1.0 menampilkan warning dari Vite 8 kalau config memakai __dirname:

(!) Your Vite config uses features that are unsupported by `configLoader: 'native'`, which is planned to become the default in a future major version of Vite:
  - `__dirname` (vitest.config.ts:27:32). Use `import.meta.dirname` instead

Ini cuma warning. Ganti __dirname dengan import.meta.dirname dan warning-nya hilang.

reporters: ['basic']

Reporter basic dihapus di 4.0. Vitest sekarang mencoba me-load-nya sebagai file:

⎯⎯⎯⎯⎯⎯⎯ Startup Error ⎯⎯⎯⎯⎯⎯⎯⎯
Error: Failed to load custom Reporter from basic
  [cause]: Error: Failed to load url basic (resolved id: basic). Does the file exist?

Pakai reporter default tanpa summary:

reporters: [['default', { summary: false }]],

Kalau kamu suka output tree dari verbose yang lama, sekarang namanya tree. verbose mencetak list datar.

test.workspace

Error: The `test.workspace` option was removed in Vitest 4. Please, migrate to `test.projects` instead.

Ganti namanya jadi projects. Isinya tetap sama.

poolOptions

Yang ini cuma warning, dan gampang terlewat:

 DEPRECATED  `test.poolOptions` was removed in Vitest 4. All previous `poolOptions` are now top-level options.

Vitest mengabaikan seluruh blok itu, jadi singleFork: true berhenti berlaku dan test kamu jalan paralel. Baris summary 5.x menunjukkan 7 workers spawned. Pengganti singleFork:

test: {
  maxWorkers: 1,
  isolate: false,
},

Seluruh file test gagal di-load

Setelah config ter-load, beberapa file gagal sebelum test di dalamnya sempat jalan. Vitest menampilkannya di bawah "Failed Suites", dan tiap file menyembunyikan semua test di dalamnya.

vi.mock atau vi.hoisted di dalam describe

Error: 1 call in "src/__tests__/nested-mock.test.ts" was defined outside of the module's top level scope:

- vi.mock("~/lib/logger") at src/__tests__/nested-mock.test.ts:4:3

Although it appears nested, it will be hoisted and executed before anything in this file. Move it to the top level to reflect its actual execution order.

vi.mock dari dulu di-hoist ke atas file, jadi vi.mock di dalam describe tidak pernah benar-benar jalan di dalamnya. Vitest 2.1.9 menerimanya tanpa warning sama sekali. Sekarang ini error, dan tidak ada option config untuk mematikannya. Pindahkan ke top level:

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

vi.mock('~/lib/logger');

describe('checkout', () => {
  it('logs the order', () => {
    // ...
  });
});

vi.hoisted() kena error dan fix yang sama.

Option test di argumen ketiga

TypeError: Signature "test(name, fn, { ... })" was deprecated in Vitest 3 and removed in Vitest 4. Please, provide options as a second argument instead.
-test('flaky upload', async () => { /* ... */ }, { retry: 2 });
+test('flaky upload', { retry: 2 }, async () => { /* ... */ });

Angka timeout biasa di argumen ketiga masih jalan. Yang pindah cuma object option.

Setelah itu beres, file yang sama mungkin berhenti di API berikutnya yang sudah dihapus. describe.sequential sudah tidak ada:

TypeError: describe.sequential is not a function

Pakai describe(name, { concurrent: false }, fn).

Test yang sekarang gagal

clearMocks aktif secara default

AssertionError: expected "vi.fn()" to be called 2 times, but got 1 times

Vitest 5 men-set clearMocks: true, jadi riwayat call setiap mock dibersihkan sebelum tiap test. Test yang menghitung call dari test sebelumnya, saat module di-load, atau di beforeAll sekarang melihat call yang lebih sedikit.

Jembatan cepat untuk mengembalikan perilaku lama:

test: {
  clearMocks: false,
},

Fix yang benar adalah assert hanya pada call yang terjadi di dalam test itu sendiri. Biasanya artinya mengubah toHaveBeenCalledTimes(2) jadi toHaveBeenCalledTimes(1) dan memindahkan setup yang penting ke beforeEach.

.resolves dan .rejects yang tidak di-await

Di 2.1.9 ini warning yang bilang akan gagal "in Vitest 3". Di 5.1.0 test-nya gagal:

Error: Promise returned by `expect(actual).resolves.toEqual(expected)` was not awaited. This assertion is asynchronous and must be awaited; otherwise, it is not guaranteed to complete before the test finishes:

await expect(actual).resolves.toEqual(expected)

Tambahkan await. Tidak ada option config untuk mengembalikan auto-await yang lama.

Perubahan assertion yang lebih kecil

Ini muncul setelah file-file di atas bisa di-load lagi:

  • mockReset() sekarang mengembalikan implementasi asli, bukan undefined (3.0). expected 'hello' to be undefined artinya test-nya mengasumsikan perilaku lama.
  • toThrowError(new Error('bad')) sekarang juga mengecek class error-nya (3.0), jadi TypeError yang dilempar tidak lagi cocok dengan Error.
  • vi.spyOn pada class dengan mockImplementation arrow function melempar TypeError: ... is not a constructor (4.0). Kirim class: .mockImplementation(class { getApples() { return 0 } }).
  • Snapshot vi.fn() sekarang mencetak [MockFunction], bukan [MockFunction spy] (4.0). Update snapshot-nya.
  • Judul test.each dengan $id tidak lagi memberi tanda kutip pada string (5.0): case a1, bukan case 'a1'.
  • .not.toThrow('') sekarang gagal untuk error apa pun yang dilempar (5.0). Pakai regex kalau maksudnya pesan kosong.

Perubahan yang tidak mencetak apa-apa

Yang ini gampang terlewat karena run-nya tetap hijau atau tidak ada yang mencetak apa pun.

environmentMatchGlobs sudah hilang dan diabaikan. File *.node.test.ts yang tadinya jalan di node sekarang jalan di happy-dom. Pakai projects:

import { configDefaults, defineConfig } from 'vitest/config';

// inside defineConfig({ test: { ... } })
test: {
  projects: [
    {
      extends: true,
      test: {
        name: 'dom',
        environment: 'happy-dom',
        exclude: [...configDefaults.exclude, '**/*.node.test.ts'],
      },
    },
    { extends: true, test: { name: 'node', environment: 'node', include: ['**/*.node.test.ts'] } },
  ],
},

Sejak 5.0, inline project mewarisi root config secara default, jadi mereka dapat setupFiles dan alias kamu tanpa extends: true. Menambahkannya tidak masalah, dan pewarisan itu jadi jelas saat config-nya dibaca.

File vitest.workspace.ts diabaikan sepenuhnya. Vitest 2 otomatis membacanya. Pindahkan isinya ke test.projects.

Option coverage all, extension, dan ignoreEmptyLines diabaikan saat runtime. TypeScript menandainya di config, CLI tidak. Hapus dan pertahankan coverage.include. Angka coverage juga bergeser karena remapping V8 yang baru. Di test project tadi, line coverage turun dari 75% ke 50% dengan test yang sama, jadi threshold coverage di CI bisa mulai gagal.

-t 'analytics module' dulu cocok dengan test bernama module di dalam describe('analytics'). Di Vitest 5 pattern itu tidak cocok dengan apa pun, semua test di-skip, dan run-nya exit 0. Pakai -t 'analytics > module'.

--reporter=json sekarang menulis ke .vitest/json/output.json, bukan stdout. Step CI yang mem-pipe-nya ke jq gagal dengan jq: parse error. Baca file-nya, atau cek dokumentasi untuk option stdout di reporter itu.

Menjalankan vitest dari subdirectory tidak lagi menemukan config di parent directory, dan kamu dapat ReferenceError: it is not defined kalau globals: true ada di config itu. Memakai --config ../vitest.config.ts tetap gagal kalau setupFiles memakai path relatif, karena path itu di-resolve dari direktori saat ini. Resolve dari file config:

setupFiles: [path.resolve(import.meta.dirname, 'vitest.setup.ts')],

Fake timer bisa bocor antar file

Vitest 5.x menyarankan isolate: false di summary run supaya lebih cepat. Dengan isolate: false (yang juga pengganti singleFork di atas), file yang memanggil vi.useFakeTimers() tanpa mengembalikannya akan membocorkan waktu palsu ke file berikutnya. Tambahkan ini di setup file:

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

UI sekarang butuh token

@vitest/ui 5 melindungi UI-nya. Membuka http://localhost:51204/__vitest__/ tanpa token mengembalikan 403. Pakai URL lengkap yang dicetak Vitest di terminal, yang ada ?token=....

Cek hasilnya

npx vitest --version
npx vitest run

Harusnya muncul vitest/5.1.0 dan tidak ada baris DEPRECATED atau Failed Suites. Bandingkan jumlah test dengan run lama. Kalau berkurang, lihat perubahan diam-diam di atas, terutama vitest.workspace.ts dan environmentMatchGlobs.

Kesimpulan

Untuk upgrade Vitest 2 ke Vitest 5, naikkan semua package @vitest/* dan Vite bersamaan, perbaiki error config, pindahkan vi.mock ke top level, lalu tangani clearMocks dan .resolves yang tidak di-await. Setelah itu bandingkan jumlah test dan coverage dengan run lama. Kalau salah satunya turun, cek perubahan diam-diam duluan.