Recap – ViTest statt Jest
Meine Erfahrung beim Umzug von Jest zu ViTest – mit dem Schwerpunkt auf Test mit dem Dateisystem. Es hat sich gelohnt und ich verrate euch warum!

Die aktuelle Situation
Beim Aufsetzen des Tech-Stacks für mein aktuelles Projekt stellte ich mir die Frage, welches Test-Framework für das NodeJS Backend zum Einsatz kommt. Ich habe in jüngster Vergangenheit öfter von ViTest als gute Alternative zu Jest gelesen. Insbesondere die Geschwindigkeit, in der Jest-Tests ausgeführt werden, hat meinen Arbeitsfluss regelmäßig behindert. Dies ging stellenweise so weit, dass ich die Tests unregelmäßig ausgeführt habe. Mit dem Fokus auf Systeme, die Gewohnheiten unterstützen, musste ich mich dringend anpassen. Denn testgetriebene Entwicklung ist ein fundamentaler Baustein meiner Arbeitsweise. Passenderweise hat ViTest als besonderen Vorteil gegenüber Jest die Performance aufgelistet – also probiere ich es einfach mal aus.
Zusätzlich zum Umstieg spielt bei dem aktuellen Projekt das Dateisystem eine zentrale Rolle. Entgegen bisheriger Nutzung von Cloud-Storage gilt es in diesem Fall, im lokalen Dateisystem zu arbeiten. Um sowohl die Ausführungszeit akzeptabel zu halten, als auch die Testisolation und -stabilität zu gewährleisten, habe ich nach einer In-Memory-Dateisystem-Lösung für die Tests gesucht und bin fündig geworden – ViTest benennt in der eigenen Dokumentation eine unterstützte Bibliothek: memfs.
In den kommenden Abschnitten beschreibe ich meinen Umzug von Jest zu ViTest inklusive der Konfiguration von ViTest mit memfs
und meinen Learnings dabei.
Der Umstieg
ViTest Set-up
Der erste große Schritt ist die Einrichtung des neuen Frameworks. Im Zusammenspiel mit TypeScript hat sich dies regelmäßig als schwierig herausgestellt. Sei es, die Importe der eigenen Dateien relativ zum src
Verzeichnis via Präfix einzurichten und darauf aus dem tests
Verzeichnis Zugriff zu haben – sowohl bei der Testausführung als auch beim AutoComplete in der IDE.
Glücklicherweise erwies sich dies bei ViTest als sehr einfach, mithilfe einer einzigen Config-Datei konnte ich alles intuitiv und einfach einrichten und es lief auf Anhieb. Hier meine vitest.config.js
im Root-Verzeichnis meines Projekts – unter resolve
findet sich das Geheimnis für den lokalen Import Alias:
import path from 'path'
export default {
test: {
globals: true,
environment: 'node',
clearMocks: true,
globalSetup: 'tests/helper/globalSetup.ts',
setupFiles: [
'dotenv/config',
'tests/helper/setupTests.ts',
'tests/helper/setupTestEnvVars.ts',
'tests/helper/customMatchers/toHaveBeenCalledWithScopes.ts',
],
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
}
Weiter interessant sind die Set-up-Konfigurationen. Ich habe mir angewöhnt, im tests
Verzeichnis einen Ordner helper
anzulegen und dort meine Testkonfiguration und weiteren Hilfsklassen abzulegen. Diesen Ordner kann ich von Projekt zu Projekt kopieren und meine allgemeine Einstellung zügig übernehmen, Zeit bei der Entwicklung sparen und schrittweise weiterentwickeln, was meinen Kunden zugutekommt.
Set-up Helper
In der globalSetup
bereite ich die InMemory-MongoDb Instanz für meine Integrationstests vor. In der Regel möchte ich mich jenseits meiner Unit-Tests absichern, dass der Durchstich funktioniert – z. B. ein API-Create-Aufruf zu Event → Event gelesen und in Read-Model → API-Get-Aufruf das Ergebnis beinhaltet.
In der setupTests
bereite ich all die Services, die ich standardmäßig gemockt wissen möchte, vor. Unter anderem auch mein Dateisystem-Mock – mehr dazu weiter unten.
In der setupTestEnvVars
definiere ich, wie der Name bereits verrät, die Werte meiner Environment-Variablen als process.env
Wert.
Danach folgen meine Custom Matcher, die ich für die Projekte benötige – oben illustriert mit customMatchers/toHaveBeenCalledWithScopes
.
Umstellung der Mocks
Ich bin sehr dankbar, dass ViTest zu 99 % die gleiche Syntax nutzt wie Jest. Somit konnte ich aus sämtlichen Jest-Aufrufen Vi-Aufrufe machen – hier das einfachste Beispiel: jest.fn()
wird vi.fn()
. Genauso ist es mit der Syntax für Mocks. Das macht den Umstieg selbst innerhalb eines Projekts schlank und einfach.
ViTest und das Dateisystem
Das Set-up
Um das In-Memory-Dateisystem zu nutzen, gilt es das NPM-Paket zu installieren
npm install -D memfs
Im Rahmen der Hilfsdateien stelle ich sicher, dass sämtliche Aufrufe auf Node-Dateisystem-Funktionen standardmäßig gemockt sind. Sowohl die normalen als auch die Promise-basierten Aufrufe. Dazu füge ich folgenden Code in die setupTests
Datei:
// --- tests/helper/setupTest.ts
// mock fs
vi.mock('node:fs', async () => {
const memfs: { fs: typeof fs } = await vi.importActual('memfs')
return { default: memfs.fs, ...memfs.fs }
})
// mock fs.promises
vi.mock('node:fs/promises', async () => {
const memfs: { fs: typeof fs } = await vi.importActual('memfs')
return { default: memfs.fs.promises, ...memfs.fs.promises }
})
// reset the state of in-memory fs on each test
beforeEach(() => {
vol.reset()
})
Diese Art und Weise funktioniert zum einen für TypeScript. Zum anderen benötigst du keine weiteren Dateien in dem tests/__mocks__
Ordner.
Der erste Test
Mit dem obigen Set-up kannst du in deinen Tests die Inhalte deines Dateisystems definieren. Ich habe ein wenig gebraucht, zu verstehen, wie ich ein leeres Verzeichnis erstelle, darum das Beispiel für einen einfachen Test, der prüft, ob das Verzeichnis wirklich leer ist:
// simulate an empty dir this way
vol.fromNestedJSON({ 'empty-dir-name': {} }, 'path-to-empty-dir')
// read dir
const result = await fs.readdir('path-to-empty-dir/empty-dir-name')
expect(result).toEqual([])
Special-Case Zugriffsrechte beim Löschen
In einem meiner Testfälle galt es, folgende Anforderungen zu prüfen:
- Löschen einer Datei, wenn sie vorhanden ist
- Schritt übergehen, wenn die Datei nicht vorhanden ist
- Wenn beim Löschen Zugriffsrechte fehlen, den Fehler zu ignorieren und fortzufahren
- Bei anderen Fehlern, den Fehler durchzureichen
Mit memfs
war es einfach die Punkte 1 und 2 zu prüfen. Leider unterstützt memfs
keine Zugriffsrechte, sodass ich für Anforderung 3 und 4 einen Work-around bauen musste, den ich hier teilen möchte.
Als Erstes habe ich mir eine Hilfsklasse für den Dateisystem-Fehler gebaut:
// test helper class
class ErrorWithCode extends Error {
public readonly code: string
constructor(message: string, code: string) {
super(message)
this.code = code
}
}
export default ErrorWithCode
In meinem Test klemme ich mich in die unlink
Funktion ein und werfe meine neue Fehlerklasse. Für Anforderung 3 mit dem Error-Code EACCES
und für Anforderung 4 einen beliebigen anderen Error-Code.
// in test
vi.spyOn(fs, 'unlink').mockImplementation(() => {
throw new ErrorWithCode('EACCES: permission denied', 'EACCES')
})
Das Ergebnis im Produktivcode ist wie folgt:
try {
await fs.unlink('some/path')
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && e.code == 'EACCES') {
// handle this case
return
}
// handle other cases
}
Special-Case Zugriffsrechte beim Schreiben
Um zu testen, wie sich das System bei fehlenden Schreibrechten verhält, kann ich ähnlich vorgehen. Ich überschreibe das passende writeFile
und werfe einen Fehler. Hierbei musst du darauf achten, ob du im Code die Variante mit Promise oder ohne nutzt und die entsprechende Methode mocken. Ich habe anfangs nicht genau hingeschaut und mich gewundert, warum mein Mock nicht funktioniert.
Hier ein Beispiel zur Veranschaulichung
import { fs } from 'memfs'
// depending on which fs method you use in your prod code
// -> mock the fs.promise.writeFile or fs.writeFile
fs.promises.writeFile = vi.fn().mockRejectedValueOnce(
new Error('Sth went wrong')
)
Fazit
Der Umstieg war außergewöhnlich einfach und schnell erledigt. Die Konfiguration der Tests und TypeScript ist problemlos in wenigen Minuten erledigt. Dadurch, dass ich bereits mit Jest vertraut war, schreiben sich die Tests wie gewohnt in ViTest.
Die Performance ist grandios. Die Ausführung blitzschnell. Ich kann meine Tests wieder im Watch-Mode laufen und mir instantan Feedback geben lassen, ob das funktioniert, was ich tue. Die Entscheidung zu wechseln war goldrichtig und kann ich dir nur ans Herz legen.
Und der großartige Bonus, mithilfe von memfs
ein In-Memory-Dateisystem zu haben, ist unschlagbar. Das hat mir viel Zeit erspart und ist für meine Testfälle mehr als genügend. Einzig nervig war die Notwendigkeit für Hacks bei den Zugriffsrechten. Gemäß den Diskussionen in den Github Threads wäre dieses Feature nicht so einfach umzusetzen und daher verständlich, dass es nicht verfügbar ist.