API-Mocking auf Netzwerk-Ebene statt auf Funktions-Ebene
Mock Service Worker fängt HTTP-Anfragen an der Netzwerkschnittstelle ab, bevor sie überhaupt versendet werden, statt einzelne JavaScript-Funktionen wie Axios-Aufrufe zu ersetzen. Das macht Vue-Tests realistischer, weil der komplette Anfrage-Antwort-Zyklus durchlaufen wird, inklusive Statuscodes, Headern und Fehlerpfaden, ohne dass der Testcode wissen muss, welche HTTP-Bibliothek eine Komponente intern verwendet.
Inhaltsverzeichnis
- 1. Mocking auf Netzwerk-Ebene statt Funktions-Ebene
- 2. Setup für Vitest
- 3. Request-Handler für typische REST-Endpoints
- 4. Der Vorteil gegenüber vi.mock() bei Axios/Fetch
- 5. Ein Komponententest mit MSW im Zusammenspiel
- 6. Browser-Setup vs. Node-Setup
- 7. Fehlerpfade und Netzwerkfehler gezielt simulieren
- 8. Wiederverwendbare Handler-Struktur pflegen
- 9. Grenzen von MSW und was es nicht ersetzt
- 10. Zusammenfassung
- 11. FAQ
1. Mocking auf Netzwerk-Ebene statt Funktions-Ebene
Klassisches Mocking mit vi.mock('axios') oder vi.mock('./api') ersetzt ein komplettes Modul durch eine Fake-Implementierung, die der Testcode selbst definiert. Das funktioniert, koppelt den Test aber eng an die konkrete Implementierung: Wechselt eine Komponente von Axios zu Fetch, oder ändert sich der interne Aufbau des API-Moduls, müssen sämtliche Mocks angepasst werden, obwohl sich am eigentlichen Verhalten der Anwendung nichts geändert hat.
Mock Service Worker (MSW) setzt an einer anderen Stelle an: Es fängt Anfragen direkt auf Netzwerkebene ab, entweder über einen echten Service Worker im Browser oder über eine Node-Interception in Testumgebungen, und beantwortet sie mit definierten Handlern. Die getestete Komponente merkt davon nichts, sie sendet eine ganz normale HTTP-Anfrage über Fetch oder Axios und bekommt eine Antwort, die aussieht wie eine echte Server-Antwort. Der Test bleibt dadurch unabhängig davon, welche HTTP-Bibliothek intern verwendet wird.
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/products/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'Teekanne',
price: 29.9,
})
}),
http.post('/api/cart/items', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ id: 'cart-item-1', ...body }, { status: 201 })
}),
http.get('/api/products/:id/reviews', () => {
return HttpResponse.json([], { status: 200 })
}),
]
2. Setup für Vitest
Die Einrichtung von MSW für Vitest erfolgt über setupServer aus msw/node, das die Handler in einem Test-Setup-File registriert. Vitest lädt dieses Setup-File automatisch vor jedem Testlauf, sodass sämtliche Testdateien standardmäßig auf die gleichen, zentral definierten Handler zugreifen, ohne dass jede Datei ihr eigenes Mocking aufbauen muss. Der Server wird vor allen Tests gestartet, nach jedem Test zurückgesetzt und nach allen Tests wieder geschlossen.
Wichtig ist, den Server-Reset nach jedem Test nicht zu vergessen, weil sonst Handler-Overrides aus einem Test in den nächsten durchsickern können. Ein sauberer Reset stellt sicher, dass jeder Test mit dem gleichen, vorhersagbaren Ausgangszustand startet, unabhängig davon, in welcher Reihenfolge die Testdateien ausgeführt werden. Gerade bei parallel laufenden Testsuiten ist das ein Detail, das leicht übersehen wird, aber schwer zu debuggende, sporadisch fehlschlagende Tests verursacht, wenn es fehlt.
3. Request-Handler für typische REST-Endpoints
Handler in MSW werden pro HTTP-Methode und Pfad definiert, wobei Pfadparameter wie :id automatisch geparst und über das params-Objekt im Handler zugänglich gemacht werden. Für typische CRUD-Endpoints reicht meist ein Handler pro Methode und Ressource: GET für Einzelabruf und Listen, POST für Erstellung, PUT oder PATCH für Aktualisierung, DELETE für Löschung. Query-Parameter lassen sich über das request-Objekt und dessen url-Eigenschaft auslesen, etwa für Filter- oder Paginierungslogik.
Für Fehlerpfade lässt sich derselbe Endpoint in einem einzelnen Test gezielt überschreiben, ohne den globalen Handler dauerhaft zu ändern. Über server.use(...) innerhalb eines konkreten Tests wird für diesen einen Testfall eine Fehlerantwort simuliert, etwa ein 500er-Statuscode oder eine Netzwerk-Fehlerantwort, während alle anderen Tests weiterhin den erfolgreichen Standard-Handler verwenden. Das macht das Testen von Error-Boundaries und Fehlermeldungen in der UI deutlich einfacher als mit manuell konstruierten Mock-Rückgabewerten.
4. Der Vorteil gegenüber vi.mock() bei Axios/Fetch
Mit vi.mock('axios') muss der Testcode für jeden getesteten Aufruf genau wissen, welche Axios-Methode intern verwendet wird, ob axios.get, eine Axios-Instanz mit axios.create() oder ein Interceptor beteiligt ist. Ändert sich diese interne Struktur, etwa weil ein Team von direkten Axios-Aufrufen auf eine zentrale API-Client-Klasse umstellt, müssen sämtliche vi.mock-Aufrufe in allen betroffenen Tests angepasst werden, selbst wenn sich am HTTP-Vertrag der Anwendung nichts geändert hat.
MSW abstrahiert genau diese Implementierungsdetails weg, weil es an der Netzwerkgrenze ansetzt statt an der JavaScript-API-Grenze. Ein Wechsel von Axios zu Fetch, oder das Einführen eines eigenen API-Wrapper-Moduls, erfordert keine Änderung an den MSW-Handlern, solange die tatsächlichen HTTP-Anfragen unverändert bleiben. Das macht Tests robuster gegenüber internen Refactorings und verringert die Kopplung zwischen Testcode und Implementierungsdetails erheblich, was gerade bei größeren Vue-Projekten mit vielen API-Aufrufen einen spürbaren Wartungsvorteil bringt.
5. Ein Komponententest mit MSW im Zusammenspiel
In einem konkreten Komponententest wird MSW meist im Hintergrund über das globale Setup aktiv, ohne dass die einzelne Testdatei etwas davon direkt importiert, außer wenn ein Handler für genau diesen Test überschrieben werden soll. Die getestete Komponente lädt Daten wie gewohnt über ihre normale API-Schicht, MSW beantwortet die Anfrage im Hintergrund, und der Test prüft anschließend, ob die Komponente die empfangenen Daten korrekt anzeigt.
Dieses Vorgehen erlaubt es, echte Async-Flows realistisch zu testen, inklusive Ladezuständen zwischen Anfrage und Antwort, weil MSW die Antwort erst nach einem echten (wenn auch schnellen) Promise-Zyklus liefert. Tests, die auf await flushPromises() oder ähnliche Hilfsfunktionen zurückgreifen, um auf die asynchrone Antwort zu warten, verhalten sich dadurch näher am tatsächlichen Laufzeitverhalten der Anwendung als bei synchron aufgelösten, manuell konstruierten Mocks.
6. Browser-Setup vs. Node-Setup
MSW unterscheidet zwischen zwei Betriebsmodi: Im Browser, etwa für Storybook oder eine lokale Entwicklungsumgebung, registriert sich ein echter Service Worker über setupWorker und fängt Anfragen ab, bevor sie den Browser verlassen. In Node-basierten Testläufern wie Vitest kommt stattdessen setupServer aus msw/node zum Einsatz, das auf einer Interception-Bibliothek basiert, weil Node keinen echten Service Worker kennt.
Der entscheidende Vorteil ist, dass dieselben Handler-Definitionen in beiden Umgebungen wiederverwendet werden können. Ein Handler-File, das für Storybook geschrieben wurde, um Komponenten isoliert mit realistischen API-Antworten zu entwickeln, lässt sich unverändert auch in Vitest-Tests einbinden. Das spart doppelte Pflege von Mock-Daten und stellt sicher, dass Entwicklungsumgebung und Testumgebung dieselben Annahmen über die API-Form teilen.
7. Fehlerpfade und Netzwerkfehler gezielt simulieren
Neben normalen Erfolgsantworten lässt sich mit MSW auch das Verhalten bei echten Netzwerkfehlern testen, etwa über HttpResponse.error(), was einen kompletten Verbindungsabbruch simuliert, im Unterschied zu einer Antwort mit Fehlerstatuscode wie 404 oder 500. Diese Unterscheidung ist wichtig, weil viele Frontend-Fehlerbehandlungen unterschiedlich auf einen abgelehnten Fetch-Promise reagieren als auf eine technisch erfolgreiche Antwort mit Fehlerstatus.
Auch Verzögerungen lassen sich gezielt simulieren, etwa über delay() aus dem MSW-Paket, um Ladezustände und Race-Conditions zwischen mehreren gleichzeitigen Anfragen realistisch zu testen. Ein Test, der prüft, ob ein Ladeindikator korrekt verschwindet, sobald die letzte von mehreren parallelen Anfragen beantwortet wurde, profitiert deutlich von einer kontrollierten, aber realistischen Verzögerung, statt sich auf zufällige Timing-Eigenschaften synchroner Mocks zu verlassen.
8. Wiederverwendbare Handler-Struktur pflegen
In größeren Projekten lohnt es sich, Handler nach Domäne zu organisieren, etwa eine Datei für Produkt-Endpoints, eine für Warenkorb-Endpoints und eine für Nutzerkonto-Endpoints, statt alle Handler in einer einzigen, schnell unübersichtlichen Datei zu sammeln. Diese Handler-Dateien lassen sich dann selektiv importieren, sodass ein Test nur die Handler lädt, die für den jeweiligen Testbereich tatsächlich relevant sind, was die Testdatei lesbarer macht und ungewollte Interaktionen zwischen unabhängigen Testbereichen reduziert.
Ein weiterer Vorteil dieser Struktur zeigt sich beim Onboarding neuer Teammitglieder: Wer die Handler-Dateien liest, bekommt gleichzeitig eine dokumentierte Übersicht über die tatsächlich verwendeten API-Endpoints und deren erwartete Antwortform, was oft aktueller ist als separate API-Dokumentation, weil die Handler direkt mit den Tests gepflegt werden müssen, um diese lauffähig zu halten.
9. Grenzen von MSW und was es nicht ersetzt
So realistisch MSW den Anfrage-Antwort-Zyklus auch simuliert, es prüft an keiner Stelle, ob die Handler-Definitionen tatsächlich mit der echten Backend-API übereinstimmen. Ein Handler kann problemlos ein Feld liefern, das die reale API längst umbenannt hat, ohne dass ein Test dies bemerkt, weil MSW lediglich das zurückgibt, was im Handler definiert wurde, unabhängig davon, ob dieses Format noch der Realität entspricht. Diese Lücke zwischen Mock und echter API wird als Contract-Drift bezeichnet und ist ein bekanntes Risiko bei jeder Form von Mocking, nicht nur bei MSW.
Um dieses Risiko zu begrenzen, ergänzen viele Teams ihre MSW-Handler um regelmäßige Contract-Tests gegen die echte API, etwa in einer separaten Testsuite, die tatsächlich gegen eine Staging-Umgebung läuft und stichprobenartig prüft, ob die angenommene Antwortform noch stimmt. MSW ersetzt also keine Integrationstests gegen ein echtes Backend, sondern ergänzt sie um eine schnelle, isolierte Ebene für die Frontend-Logik, während die Verantwortung für die Korrektheit der Handler-Definitionen selbst weiterhin bei den Entwicklern liegt, die sie pflegen.
| Aspekt | vi.mock() (Funktionsebene) | MSW (Netzwerkebene) |
|---|---|---|
| Kopplung | an konkrete HTTP-Bibliothek | unabhängig von Axios/Fetch |
| Realismus | manuell konstruierte Rückgabewerte | echter Anfrage-Antwort-Zyklus |
| Wiederverwendung | pro Testdatei neu definiert | zentrale Handler-Dateien |
| Fehlerpfade | muss manuell simuliert werden | HttpResponse.error() und Statuscodes |
| Storybook-Kompatibilität | nicht direkt nutzbar | gleiche Handler wiederverwendbar |
Mironsoft
Vue-Architektur, Composition API und Nuxt-Performance
Vue-Anwendungen, die mit jedem Feature nicht komplizierter werden?
Wir prüfen bestehende Vue- und Nuxt-Projekte auf unstrukturierte Composables, ungenutzte Reaktivität und aufgeblähte Bundles und bauen daraus eine Architektur, die neue Features aufnimmt, ohne die Codebasis unübersichtlicher zu machen.
Architektur-Review
Composables, State-Management und Komponentenstruktur auf Wartbarkeit prüfen.
Performance-Audit
Reaktivitäts-Overhead, Bundle-Größe und Nuxt-Rendering-Strategie systematisch optimieren.
Nuxt-Integration
SSR/SSG-Setup und API-Anbindung robust und typsicher aufbauen.
10. Zusammenfassung
MSW für Vue-Tests: Das Wichtigste auf einen Blick
Ansatzpunkt
MSW mockt auf Netzwerkebene, nicht auf Funktionsebene
Setup
setupServer aus msw/node, Reset nach jedem Test nicht vergessen
Handler
pro HTTP-Methode und Pfad definiert, in Tests gezielt überschreibbar
Vorteil
unabhängig davon, ob Axios, Fetch oder ein eigener API-Wrapper verwendet wird