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
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.
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.mockmock.mockClear()
expect(state).toBe(mock.mock) // besteht in Vitest, scheitert in JestGlobals 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 VitestbeforeEach(() => { 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:
// Jestjest.mock('./some-path', () => 'hello')
// Vitestvi.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.
# Jestvitest -t 'math adds' # trifft nicht mehr
# Vitestvitest -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. Aufasync/awaitumschreiben oder einwickeln:it('works', () => new Promise(done => { /* ... */ done() })).- Es gibt keinen
jest-Typ-Namespace. Auslet fn: jest.Mock<...>wirdimport type { Mock } from 'vitest'. - Jests Legacy-Fake-Timer werden nicht unterstützt.
jest.setTimeout(5000)wird zuvi.setConfig({ testTimeout: 5_000 }).jest.replacePropertyhat keine direkte Entsprechung; nimmvi.stubEnvodervi.spyOn.JEST_WORKER_IDwird zuVITEST_POOL_ID(immer ≤maxWorkers). Beachte:VITEST_WORKER_IDexistiert ebenfalls, bedeutet aber etwas anderes — eine eindeutige ID je erzeugtem Worker, nicht durchmaxWorkersbegrenzt.
Solltest du überhaupt migrieren?
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
- 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.
- Die globals-Frage bewusst entscheiden. Schaltest du globals aus, prüfe das DOM-Cleanup, bevor du einem einzigen Komponententest traust.
- Jedes
mockResetundresetMocksprüfen. Das ist die Stelle, die Bugs ausliefert. Greppe danach und kontrolliere, ob jede Assertion noch das bedeutet, was sie sagt. - Nach
__mocks__undrequireActualgreppen. Bestätige, dass jeder Mock wirklich greift — brich das echte Modul absichtlich und prüfe, ob der Test fehlschlägt. - Hook-Reihenfolge erneut prüfen in jeder Suite mit verschachtelten
describe-Blöcken, die Zustand teilen. - 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.





Aus der Community
Diskussion im Fediverse
Antworten von Mastodon und Bluesky — direkt aus dem offenen Netz, ohne Tracking.
Antworten werden geladen …
Noch keine Antworten. Starte die Diskussion:
Antworten konnten gerade nicht geladen werden.