Zurück zum Blog
Programmierung
FortgeschrittenFürJavaScript DevelopersFrontend EngineersNode.js Developers
6 min

Von Jest zu Vitest migrieren: Was wirklich kaputtgeht

jest durch vi ersetzen — und der Großteil einer Jest-Suite läuft unverändert unter Vitest. Die Probleme sind nicht die Aufrufe, die fehlschlagen, sondern die, die weiter durchlaufen und dabei etwas anderes bedeuten. Ein Migrationsleitfaden, sortiert danach, wie jeder Unterschied auffällt.

vitestjestjavascript-testingtest-migrationvitemocking
Titelbild: Von Jest zu Vitest migrieren: Was wirklich kaputtgeht
Inhalt

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'

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

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'

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()) })

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' },
},
})

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'] } },
},
})

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' }))

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

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

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'

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.

Häufig gestellte Fragen

Ist die Migration von Jest zu Vitest schwierig?

Der mechanische Teil ist einfach, der semantische nicht. Vitest wurde bewusst mit einer Jest-kompatiblen API entworfen, sodass die meisten Testdateien laufen, sobald man das globale jest durch vi ersetzt — viele sogar ganz ohne Änderung, wenn man die globals-Option aktiviert. Zeit kostet eine kurze Liste von Verhaltensunterschieden, die keine Fehler erzeugen: mockReset() stellt die ursprüngliche Implementierung wieder her statt einer leeren, Hooks laufen als Stack statt sequenziell, Testnamen werden mit '>' statt mit Leerzeichen verbunden, und Module in __mocks__ werden nicht automatisch geladen. Plane deine Migrationszeit für das Prüfen dieser Punkte ein, nicht für Suchen-und-Ersetzen.

Was ist der Unterschied zwischen mockReset in Jest und Vitest?

Sie tun das Gegenteil voneinander. Jests mockReset ersetzt die Mock-Implementierung durch eine leere Funktion, die undefined zurückgibt. Vitests mockReset setzt die Mock-Implementierung auf ihr Original zurück — ein mit vi.fn(impl) erzeugter Mock bekommt also impl zurück. Ein Test, der das Verhalten des leeren Mocks geprüft hat, führt jetzt still die echte Implementierung aus und besteht in vielen Fällen trotzdem. Das ist der gefährlichste Unterschied der Migration, weil nichts darauf hinweist. Prüfe jeden mockReset-Aufruf und jede Stelle, an der du dich auf resetMocks in der Konfiguration verlässt.

Warum sind describe und it in Vitest nicht definiert?

Weil Vitest die globals-API nicht aktiviert, die Jest standardmäßig einschaltet. Du hast zwei Möglichkeiten: globals in der Vitest-Konfiguration auf true setzen, was das Jest-ähnliche Verhalten wiederherstellt, oder in jeder Testdatei import { describe, it, expect } from 'vitest' schreiben. Der Import ist langfristig die sauberere Wahl, aber eine Folge des Deaktivierens solltest du kennen: verbreitete Bibliotheken wie testing-library führen ihr automatisches DOM-Cleanup nicht mehr aus. Das wirft keinen Fehler — es zeigt sich darin, dass Tests Zustand ineinander verschleppen, was eine deutlich unangenehmere Art ist, es herauszufinden.

Unterstützt Vitest den done-Callback?

Nein. Vitest unterstützt den Callback-Stil für Testdeklarationen nicht. Schreibe sie als async/await-Funktionen um, was meist ohnehin klarer ist. Wenn ein Test die Callback-Form wirklich braucht — etwa beim Testen eines Event-Emitters — kannst du ihn in ein Promise wickeln: it('works', () => new Promise(done => { /* ... */ done() })). Das gehört zu den lauten Fehlern, du findest also jede Stelle beim ersten Lauf statt später in Produktion.

Funktionieren meine __mocks__-Ordner in Vitest weiter?

Nur, wenn du sie ausdrücklich anforderst. Jest lädt Module aus einem __mocks__-Verzeichnis im Wurzelverzeichnis automatisch; Vitest lädt sie nicht, solange für das Modul kein vi.mock() aufgerufen wird. Willst du das Jest-Verhalten für die gesamte Suite, rufe die Mocks in einer Datei auf, die in setupFiles gelistet ist. Der Fehlermodus ist hier still und unangenehm: Es wird kein Fehler geworfen, der Mock wird schlicht nicht angewendet, und das echte Modul läuft — bei einem Zahlungs-Client oder einer HTTP-Schicht genau der Test, den du nicht laufen lassen wolltest.

Ist Vitest tatsächlich schneller als Jest?

Das hängt vollständig davon ab, ob du bereits Vite verwendest — und das ist die ehrliche Antwort, keine Ausflucht. Vitest verwendet die Vite-Konfiguration, die Plugins und die Transform-Pipeline deines Projekts wieder; die Dokumentation beschreibt eine gemeinsame Transformations-Pipeline über Dev, Build und Test, während Jest und Vite sonst zwei getrennte Konfigurationen bedeuten. Im Watch-Mode durchläuft Vitest den Modulgraphen und führt nur die zugehörigen Tests erneut aus, so wie HMR in Vite. In einem webpack- oder reinen Node-Projekt bekommst du ESM-Unterstützung und eine angenehmere Entwicklungserfahrung, hast aber für einen viel kleineren Teil der Geschwindigkeitsgeschichte die volle Migration bezahlt. Entscheide auf dieser Grundlage, nicht anhand fremder Benchmarks.

Aus der Community

Diskussion im Fediverse

Antworten von Mastodon und Bluesky — direkt aus dem offenen Netz, ohne Tracking.

Antworten werden geladen …

ENDE