DEV Community

Aleksei Aleinikov
Aleksei Aleinikov

Posted on Originally published at alekseialeinikov.com on

Von Jest zu Vitest migrieren: Was wirklich kaputtgeht

Von Jest zu Vitest migrieren: Was wirklich kaputtgeht

jest durch vi ersetzen bringt dich fast ans Ziel. Vitest wurde bewusst mit einer Jest-kompatiblen API gebaut, und ein großer Teil jeder Suite läuft unverändert.

Genau diese Kompatibilität ist auch das Problem. Die Aufrufe, die fehlschlagen, sind der einfache Teil — die erledigst du an einem Nachmittag, weil der Lauf stoppt und es dir sagt. Teuer sind die Aufrufe, die weiterlaufen und etwas anderes bedeuten.

Hier steht, was wirklich kaputtgeht — sortiert danach, wie es kaputtgeht.

Drei Arten, wie eine Suite bricht

Drei Spalten: laute Fehler wie done-Callbacks und jest-Namespace-Typen, still falsches Verhalten wie mockReset und Hook-Reihenfolge, und still fehlendes Verhalten wie nicht automatisch geladene __mocks__.
Nur die erste Spalte ist billig. Die anderen beiden sind der Grund, warum ein grüner Lauf am ersten Tag nichts beweist.

Laute Unterschiede werfen sofort. Die kannst du nicht ausliefern.

Still falsche Unterschiede führen denselben Aufruf mit anderer Semantik aus. Der Test besteht — und prüft jetzt etwas anderes.

Still fehlende Unterschiede bedeuten, dass etwas, das du konfiguriert hast, einfach nicht passiert. Kein Fehler, kein Mock, echtes Modul.

Plane deine Zeit um die mittlere Spalte herum.

Die Falle: mockReset() bedeutet das Gegenteil

Wenn du vor dem Start nur eine Sache liest, dann diese.

Gegenüberstellung: In Jest ersetzt mockReset die Implementierung durch eine leere Funktion, die undefined zurückgibt; in Vitest wird die ursprüngliche Implementierung wiederhergestellt.
Identischer Aufruf, gegenteiliges Ergebnis. Nichts in der Ausgabe warnt dich, dass sich die Bedeutung geändert hat.

Jests mockReset ersetzt die Mock-Implementierung durch eine leere Funktion, die undefined zurückgibt. Vitests mockReset setzt die Implementierung auf ihr Original zurück — ein mit vi.fn(impl) erzeugter Mock bekommt impl zurück.


const fn = vi.fn(() => 'real')

fn.mockReset()

fn() // Jest-Semantik: undefined

       // Vitest-Semantik: 'real'

Enter fullscreen mode Exit fullscreen mode

Ein Test, der das Verhalten des leeren Mocks geprüft hat, führt jetzt die echte Implementierung aus. In vielen Fällen besteht er trotzdem — und prüft nicht mehr, was sein Name behauptet.

Verwandt und ebenso leise: mock.mock ist in Vitest persistent. Jest erzeugt das Mock-State-Objekt bei .mockClear() neu, weshalb man es immer als Getter lesen muss. Vitest hält eine persistente Referenz — das hier besteht in Vitest und scheitert in Jest:


const mock = vi.fn()

const state = mock.mock

mock.mockClear()

expect(state).toBe(mock.mock) // besteht in Vitest, scheitert in Jest

Enter fullscreen mode Exit fullscreen mode

Globals sind aus

Jest aktiviert seine globals-API standardmäßig. Vitest nicht. Entweder globals: true in der Konfiguration setzen, oder importieren, was du benutzt:


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

Enter fullscreen mode Exit fullscreen mode

Der Import ist langfristig die bessere Form. Aber kenne den Nebeneffekt: testing-library führt sein automatisches DOM-Cleanup nicht mehr aus. Das wirft keinen Fehler. Es zeigt sich darin, dass Tests sich gegenseitig verschmutzen — eine erheblich unangenehmere Art, eine Konfigurationsentscheidung zu entdecken.

Hooks laufen als Stack

In Hooks verstecken sich zwei getrennte Änderungen.

Erstens dürfen beforeAll und beforeEach in Vitest eine Teardown-Funktion zurückgeben. Das macht knappe Arrow-Bodies gefährlich, denn ein impliziter Rückgabewert wird jetzt als Teardown interpretiert:

// Jest: in Ordnung. Vitest: der Rückgabewert gilt als Teardown-Funktion.

beforeEach(() => setActivePinia(createTestingPinia()))

// Korrekt in Vitest

beforeEach(() => { setActivePinia(createTestingPinia()) })

Enter fullscreen mode Exit fullscreen mode

Zweitens: Jest führt Hooks sequenziell aus, Vitest als Stack. Wenn du verschachtelte describe-Blöcke mit reihenfolgeabhängigen Hooks hast, ist das eine echte Verhaltensänderung. So bekommst du Jests Reihenfolge zurück:


export default defineConfig({

  test: {

    sequence: { hooks: 'list' },

  },

})
Enter fullscreen mode Exit fullscreen mode

Mocks, die still nicht stattfinden

__mocks__ ist nicht automatisch. Module in einem __mocks__-Verzeichnis im Wurzelverzeichnis werden nur geladen, wenn vi.mock() aufgerufen wird. Jest lädt sie für dich. Willst du das Jest-Verhalten suiteweit, rufe die Mocks in einer Datei auf, die in setupFiles gelistet ist.

Mocking wird nicht auf Drittbibliotheken übertragen. Wo Jest einen Modul-Mock automatisch auf externe Bibliotheken anwendet, die dasselbe Modul importieren, musst du es Vitest ausdrücklich sagen:


export default defineConfig({

  test: {

    server: { deps: { inline: ['lib-name'] } },

  },

})
Enter fullscreen mode Exit fullscreen mode

Mock-Factories geben ein Objekt zurück, keinen Wert. In Jest ist der Rückgabewert der Factory der Default-Export. In Vitest muss es ein Objekt mit jedem benannten Export sein:

// Jest

jest.mock('./some-path', () => 'hello')

// Vitest

vi.mock('./some-path', () => ({ default: 'hello' }))

Enter fullscreen mode Exit fullscreen mode

jest.requireActual wird zu vi.importActual — und ist asynchron. Das await übersieht man leicht:


const { cloneDeep } = await vi.importActual('lodash/cloneDeep')

Enter fullscreen mode Exit fullscreen mode

Testnamen werden mit > verbunden

Vitest verbindet Suite- und Testnamen mit >, damit Suites leichter zu unterscheiden sind. Jest verbindet sie mit einem Leerzeichen. Das beißt an zwei Stellen: bei expect.getState().currentTestName und bei jedem -t- bzw. testNamePattern-Filter, der über die Grenze reicht.

Terminal window

# Jest

vitest -t 'math adds' # trifft nicht mehr

# Vitest

vitest -t 'math > adds'

Enter fullscreen mode Exit fullscreen mode

Sicherste Lösung für CI-Skripte: ein einzelnes Segment matchen (-t adds) oder einen Platzhalter dazwischensetzen (-t 'math.*adds').

Die lauten

Die stoppen den Lauf, kosten dich also einen Nachmittag und nicht mehr.

  • done-Callbacks werden nicht unterstützt. Auf async/await umschreiben oder einwickeln: it('works', () => new Promise(done => { /* ... */ done() })).
  • Es gibt keinen jest-Typ-Namespace. Aus let fn: jest.Mock<...> wird import type { Mock } from 'vitest'.
  • Jests Legacy-Fake-Timer werden nicht unterstützt.
  • jest.setTimeout(5000) wird zu vi.setConfig({ testTimeout: 5_000 }).
  • jest.replaceProperty hat keine direkte Entsprechung; nimm vi.stubEnv oder vi.spyOn.
  • JEST_WORKER_ID wird zu VITEST_POOL_ID (immer ≤ maxWorkers). Beachte: VITEST_WORKER_ID existiert ebenfalls, bedeutet aber etwas anderes — eine eindeutige ID je erzeugtem Worker, nicht durch maxWorkers begrenzt.

Solltest du überhaupt migrieren?

Checkliste: bereits auf Vite, ESM gewünscht, große Watch-Schleife sprechen für die Migration; webpack oder reines Node und Jest-only-Plugins sprechen dagegen.
Das Geschwindigkeitsargument hängt an einer einzigen Frage: Bist du schon auf Vite?

Diesen Teil überspringen die meisten Migrationsartikel — und er entscheidet alles.

Vitest ist schnell, weil es deine Vite-Pipeline wiederverwendet. Die eigene Dokumentation sagt es direkt: Es ist ein Test-Runner, der über vite.config.js dieselbe Konfiguration wie deine App nutzt und sich eine gemeinsame Transformations-Pipeline über Dev, Build und Test teilt — während Jest und Vite dich sonst zwingen, zwei getrennte Pipelines zu konfigurieren. Im Watch-Mode durchläuft Vitest den Modulgraphen und führt nur die zugehörigen Tests erneut aus, so wie HMR in Vite funktioniert.

In einem webpack- oder reinen Node-Projekt bekommst du weiterhin erstklassiges ESM und eine bessere Entwicklungserfahrung. Aber du hast die volle Migration für einen deutlich kleineren Teil des Nutzens bezahlt. Das Vitest-Team positioniert sich hier sorgfältig: Ziel ist der Runner der Wahl für Vite-Projekte — und eine solide Alternative auch für Projekte ohne Vite. Das kann trotzdem richtig sein — entscheide dich dann für ESM und Ergonomie, nicht wegen eines Benchmarks aus einer fremden Vite-App.

Noch eine Vorabprüfung: Inventarisiere deine Jest-only-Plugins und -Serializer , bevor du anfängst. Vue-Projekte brauchen zum Beispiel jest-serializer-vue in snapshotSerializers, sonst füllen sich Snapshots mit escapten Anführungszeichen.

Eine Migrationsreihenfolge, die funktioniert

  1. Erst laut zum Laufen bringen. Installieren, Konfiguration auf dein Test-Glob zeigen lassen, einmal laufen lassen, alles reparieren, was wirft. Das ist der billige Teil.
  2. Die globals-Frage bewusst entscheiden. Schaltest du globals aus, prüfe das DOM-Cleanup, bevor du einem einzigen Komponententest traust.
  3. Jedes mockReset und resetMocks prüfen. Das ist die Stelle, die Bugs ausliefert. Greppe danach und kontrolliere, ob jede Assertion noch das bedeutet, was sie sagt.
  4. Nach __mocks__ und requireActual greppen. Bestätige, dass jeder Mock wirklich greift — brich das echte Modul absichtlich und prüfe, ob der Test fehlschlägt.
  5. Hook-Reihenfolge erneut prüfen in jeder Suite mit verschachtelten describe-Blöcken, die Zustand teilen.
  6. CI-Filter reparieren , die -t über eine Suite-Grenze hinweg nutzen.

Schritt 4 verdient Nachdruck, weil er den üblichen Instinkt umkehrt: lass den Test absichtlich fehlschlagen. Ein Mock, der still nicht griff, sieht exakt aus wie einer, der griff — bis zu dem Moment, in dem der echte HTTP-Call rausgeht.

Das Fazit

Die Jest-kompatible API ist wirklich gute Arbeit, und sie ist der Grund, warum diese Migration in Tagen statt Wochen gemessen wird. Sie ist auch der Grund, warum man die Migration zu früh für abgeschlossen erklärt.

Eine grüne Suite nach dem Umstieg bedeutet nicht, dass die Suite noch prüft, was sie vorher prüfte. Betrachte den ersten grünen Lauf als Beginn der Prüfung, nicht als Ende der Migration.

Für dasselbe Gespräch auf der Python-Seite behandelt der pytest-Praxisleitfaden Fixtures und dieselbe Klasse von „besteht, sagt aber nichts”-Fehlern. Und wenn du JavaScript-Tooling breiter modernisierst: Biome als Ersatz für ESLint und Prettier ist derselbe Trade-off in einer anderen Ecke der Toolchain.

Ursprünglich veröffentlicht auf alekseialeinikov.com

Top comments (0)