---
title: "Von Jest zu Vitest migrieren: Was wirklich kaputtgeht"
description: "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."
author: Aleksei Aleinikov
date: 2026-09-18
lang: de
tags: [vitest, jest, javascript-testing, test-migration, vite, mocking]
canonical: https://www.alekseialeinikov.com/de/blog/topics/programming/von-jest-zu-vitest-migrieren
source: alekseialeinikov.com
---

# 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

<figure>
  <img src="/blog/jest-vitest-breakage-classes.webp" alt="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__." width="1200" height="700" loading="lazy" decoding="async">
  <figcaption>Nur die erste Spalte ist billig. Die anderen beiden sind der Grund, warum ein grüner Lauf am ersten Tag nichts beweist.</figcaption>
</figure>

**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.

<figure>
  <img src="/blog/jest-vitest-mockreset-trap.webp" alt="Gegenüberstellung: In Jest ersetzt mockReset die Implementierung durch eine leere Funktion, die undefined zurückgibt; in Vitest wird die ursprüngliche Implementierung wiederhergestellt." width="1200" height="700" loading="lazy" decoding="async">
  <figcaption>Identischer Aufruf, gegenteiliges Ergebnis. Nichts in der Ausgabe warnt dich, dass sich die Bedeutung geändert hat.</figcaption>
</figure>

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.

```js
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:

```js
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:

```js
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:

```js
// 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:

```js
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:

```js
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:

```js
// 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:

```js
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.

```bash
# 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?

<figure>
  <img src="/blog/jest-vitest-should-migrate.webp" alt="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." width="1200" height="700" loading="lazy" decoding="async">
  <figcaption>Das Geschwindigkeitsargument hängt an einer einzigen Frage: Bist du schon auf Vite?</figcaption>
</figure>

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](https://www.alekseialeinikov.com/de/blog/topics/programming/pytest-in-der-praxis-2026-python-testing-guide) 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](https://www.alekseialeinikov.com/de/blog/topics/programming/biome-2026-eslint-prettier-ersetzen-benchmark) ist derselbe Trade-off in einer anderen Ecke der Toolchain.
