Mock Frameworks für TypeScript & Vite: Ein Überlebensratgeber

Von Boris Sander5. Dezember 2025
Mock Frameworks für TypeScript & Vite: Ein Überlebensratgeber

Mock Frameworks für TypeScript & Vite: Ein Überlebensratgeber

Oder: Wie ich lernte, die Bombe zu lieben und aufhörte, mir Sorgen um ungetesteten Code zu machen


Einleitung

Willkommen in der wunderbaren Welt des Testens, wo dein Code entweder funktioniert oder – wie die meisten Beziehungen – in einem spektakulären Feuerball endet, sobald er in Produktion geht. Aber keine Sorge: Mit den richtigen Mock-Frameworks kannst du zumindest so tun, als hättest du alles unter Kontrolle.

Schreib Tests. Nicht zu viele. Hauptsächlich Integration. — Guillermo Rauch, der Mann, der offenbar nie versucht hat, einen Legacy-Monolithen zu testen


Teil 1: Vitest – Der neue Sheriff in der Stadt

Warum Vitest?

Jest ist tot. Okay, nicht wirklich – aber Vitest ist schneller, moderner und wurde speziell für Vite entwickelt. Es ist wie der Unterschied zwischen einem Fax und einer E-Mail: Beides funktioniert, aber nur eines davon lässt dich nicht wie ein Fossil aussehen.

Vitest bietet:

  • Native ESM-Unterstützung (weil CommonJS so 2015 ist)
  • Blitzschnelle Ausführung durch Vite's HMR
  • Jest-kompatible API (für alle, die ihre Muskelgedächtnis nicht umtrainieren wollen)
  • TypeScript-Support out of the box

Die heilige Dreifaltigkeit des Mockens

1. vi.fn() – Der Alleskönner

import {vi, describe, it, expect} from 'vitest';

const mockCallback = vi.fn((x: number) => x + 1);

describe('Mein brillanter Code', () => {
   it('ruft den Callback auf (hoffentlich)', () => {
      [1, 2, 3].forEach(mockCallback);

      expect(mockCallback).toHaveBeenCalledTimes(3);
      expect(mockCallback.mock.results[0].value).toBe(2);
   });
});

2. vi.spyOn() – Der stille Beobachter

Wenn du wissen willst, was dein Code wirklich treibt, ohne ihn zu ersetzen. Wie eine Überwachungskamera, nur legal.

import {vi, describe, it, expect} from 'vitest';

const calculator = {
   add: (a: number, b: number) => a + b,
};

describe('Calculator Surveillance', () => {
   it('beobachtet die Addition', () => {
      const spy = vi.spyOn(calculator, 'add');

      calculator.add(2, 3);

      expect(spy).toHaveBeenCalledWith(2, 3);
      spy.mockRestore(); // Spuren verwischen
   });
});

3. vi.mock() – Der Modul-Terminator

Für wenn du ein ganzes Modul ersetzen musst, weil es sich schlecht benimmt:

import {vi} from 'vitest';
import type * as ApiModule from './api';

// Wird an den Anfang der Datei gehoistet – wie dein Kater nach einer Firmenfeier
vi.mock('./api', async () => {
   const actual = await vi.importActual<typeof ApiModule>('./api');
   return {
      ...actual,
      fetchData: vi.fn().mockResolvedValue({data: 'gemockte Daten'}),
   };
});

TypeScript-Typsicherheit beim Mocken

Das Problem: TypeScript weiß nicht, dass deine gemockte Funktion plötzlich mockReturnValue kann.

Die Lösung: vi.mocked()

import {vi, describe, it, expect} from 'vitest';
import {fetchUser} from './userService';

vi.mock('./userService');

describe('User Service Tests', () => {
   it('gibt einen gemockten User zurück', async () => {
      // TypeScript ist jetzt glücklich
      vi.mocked(fetchUser).mockResolvedValue({
         id: 1,
         name: 'Hans Testermann',
      });

      const user = await fetchUser(1);
      expect(user.name).toBe('Hans Testermann');
   });
});

Teil 2: Mock Service Worker (MSW) – Der Netzwerk-Ninja

Das Problem mit traditionellem API-Mocking

Du könntest vi.mock('axios') verwenden. Du könntest auch mit einem Löffel ein Loch durch eine Wand graben. Beides funktioniert technisch, aber nur eines davon ist intelligent.

Warum MSW?

MSW fängt HTTP-Requests auf der Netzwerkebene ab. Das bedeutet:

  • Dein Code bleibt unberührt – Kein Stubbing von fetch oder axios
  • DevTools zeigen die Requests – Debugging ohne Blindflug
  • Ein Mock für alles – Browser, Node.js, Tests, Entwicklung

Setup für Vitest + MSW

// src/mocks/handlers.ts
import {http, HttpResponse} from 'msw';

export const handlers = [
   http.get('https://api.example.com/users', () => {
      return HttpResponse.json([
         {id: 1, name: 'Alice'},
         {id: 2, name: 'Bob (der mit dem Bug)'},
      ]);
   }),

   http.post('https://api.example.com/users', async ({request}) => {
      const body = await request.json();
      return HttpResponse.json(
         {id: 3, ...body},
         {status: 201}
      );
   }),

   // Fehler simulieren – weil Murphy's Law real ist
   http.get('https://api.example.com/unstable', () => {
      return HttpResponse.error();
   }),
];
// src/mocks/server.ts
import {setupServer} from 'msw/node';
import {handlers} from './handlers';

export const server = setupServer(...handlers);
// vitest.setup.ts
import {beforeAll, afterEach, afterAll} from 'vitest';
import {server} from './src/mocks/server';

beforeAll(() => server.listen({onUnhandledRequest: 'error'}));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Request-Override pro Test

import {http, HttpResponse} from 'msw';
import {server} from '../mocks/server';

it('behandelt Server-Fehler wie ein Profi', async () => {
   // Temporärer Override für diesen Test
   server.use(
      http.get('https://api.example.com/users', () => {
         return HttpResponse.json(
            {message: 'Alles ist kaputt'},
            {status: 500}
         );
      })
   );

   // Test deine Error-Handling-Logik
   await expect(fetchUsers()).rejects.toThrow();
});

Teil 3: Frontend-Tests – Wo ziehen wir die Grenze?

Die Testing Trophy vs. Testing Pyramid

Die alte Pyramide sagte: „Viele Unit-Tests, wenige E2E-Tests."

Kent C. Dodds kam 2018 und sagte: „Quatsch. Integration ist König."

Die Testing Trophy priorisiert:

        🏆
       E2E
    ╔═════════╗
    ║ Integ-  ║
    ║ ration  ║  ← Hier ist die Party
    ╚═════════╝
    ╔═════════╗
    ║  Unit   ║
    ╚═════════╝
    ╔═════════╗
    ║ Static  ║  ← TypeScript & ESLint
    ╚═════════╝

Die goldene Regel

„Je mehr deine Tests dem echten Benutzerverhalten ähneln, desto mehr Vertrauen geben sie dir."

Wo sind die sinnvollen Grenzen?

✅ Was du testen solltest

KategorieBeispielWarum
Business-LogikPreisberechnung, ValidierungHier verstecken sich die teuren Bugs
User FlowsLogin, CheckoutDer Benutzer macht das täglich
Edge CasesLeere Listen, NetzwerkfehlerMurphy schläft nie
AccessibilityKeyboard-NavigationWeil wir keine Monster sind

❌ Was du NICHT testen solltest

KategorieBeispielWarum nicht
ImplementierungsdetailsCSS-KlassennamenÄndern sich ständig, Tests brechen
Third-Party-BibliothekenReact Router funktioniertDie testen das selbst
Triviale Getter/SettergetName() { return this.name }Wenn das kaputt geht, hast du größere Probleme
Snapshot-Tests für allesJede KomponenteSnapshot-Blindheit ist real

Die 80/20-Regel für Frontend-Tests

Aufwand vs. Vertrauen

100% Coverage ════════════════════════╗
                                      ║ ← Wahnsinn liegt hier
80% Coverage  ════════════════╗       ║
                              ║       ║
                              ║ Dimishing
                              ║ Returns
60% Coverage  ════════╗       ║       ║
                      ║       ║       ║
                      ║       ║       ║
              Sweet   ║       ║       ║
              Spot    ║       ║       ║
──────────────────────╩───────╩───────╩────→ Aufwand

Teil 4: Best Practices – Von Leuten, die schon gelitten haben

1. Arrange-Act-Assert (AAA)

it('fügt einen Artikel zum Warenkorb hinzu', () => {
   // Arrange – Bereite die Bühne vor
   const cart = new ShoppingCart();
   const product = {id: 1, name: 'Kaffee', price: 9.99};

   // Act – Führe die Aktion aus
   cart.addItem(product);

   // Assert – Überprüfe das Ergebnis
   expect(cart.items).toHaveLength(1);
   expect(cart.total).toBe(9.99);
});

2. Jeder Test sollte isoliert sein

// ❌ SCHLECHT: Tests beeinflussen sich gegenseitig
let counter = 0;

it('Test 1', () => {
   counter++;
   expect(counter).toBe(1);
});

it('Test 2', () => {
   expect(counter).toBe(0); // BOOM! Fehlschlag
});

// ✅ GUT: Setup in beforeEach
describe('Counter Tests', () => {
   let counter: number;

   beforeEach(() => {
      counter = 0;
   });

   it('Test 1', () => {
      counter++;
      expect(counter).toBe(1);
   });

   it('Test 2', () => {
      expect(counter).toBe(0); // Funktioniert!
   });
});

3. Testing Library: User-First

import {render, screen, userEvent} from '@testing-library/react';
import {LoginForm} from './LoginForm';

it('zeigt Fehlermeldung bei ungültiger E-Mail', async () => {
   const user = userEvent.setup();
   render(<LoginForm / >);

   // ❌ SCHLECHT: Implementation-Detail
   // const input = container.querySelector('input.email-input');

   // ✅ GUT: Wie ein User
   const emailInput = screen.getByLabelText(/e-mail/i);
   await user.type(emailInput, 'keine-email');
   await user.click(screen.getByRole('button', {name: /anmelden/i}));

   expect(screen.getByRole('alert')).toHaveTextContent(/ungültige e-mail/i);
});

4. Mock nur das Nötigste

// ❌ SCHLECHT: Alles mocken
vi.mock('react');
vi.mock('react-dom');
vi.mock('./utils');
vi.mock('./helpers');
vi.mock('./services');
// Herzlichen Glückwunsch, du testest jetzt hauptsächlich deine Mocks

// ✅ GUT: Nur externe Abhängigkeiten
vi.mock('./api'); // API-Calls mocken
// Den Rest laufen lassen

Teil 5: TDD – Test Driven Development

Der Red-Green-Refactor-Zyklus

    ╔═══════════════════════════════════════════════════════╗
    ║                                                       ║
    ▼                                                       ║
┌───────┐    ┌───────┐    ┌───────────┐                    ║
│  RED  │───▶│ GREEN │───▶│ REFACTOR  │────────────────────╯
└───────┘    └───────┘    └───────────┘
Schreib     Schreib       Verbessere
einen       Code, der     den Code
Test, der   den Test      (Tests
fehlschlägt besteht       bleiben grün)

TDD im Frontend: Ein Beispiel

Anforderung: Eine Suchfunktion, die Ergebnisse filtert

// Schritt 1: RED – Test schreiben
import {filterProducts} from './search';

describe('filterProducts', () => {
   it('filtert Produkte nach Suchbegriff', () => {
      const products = [
         {id: 1, name: 'Laptop'},
         {id: 2, name: 'Lapislazuli'},
         {id: 3, name: 'Telefon'},
      ];

      const result = filterProducts(products, 'Lap');

      expect(result).toHaveLength(2);
      expect(result[0].name).toBe('Laptop');
   });
});
// Schritt 2: GREEN – Minimaler Code
export function filterProducts(
   products: Product[],
   query: string
): Product[] {
   return products.filter((p) =>
      p.name.toLowerCase().includes(query.toLowerCase())
   );
}
// Schritt 3: REFACTOR – Verbessern
export function filterProducts(
   products: Product[],
   query: string
): Product[] {
   if (!query.trim()) return products;

   const normalizedQuery = query.toLowerCase();
   return products.filter((product) =>
      product.name.toLowerCase().includes(normalizedQuery)
   );
}

Wann TDD sinnvoll ist

SituationTDD empfohlen?Begründung
Neue Business-Logik✅ JaKlare Anforderungen, testbar
Bug-Fix✅ JaErst Test, der Bug reproduziert
UI-Prototyping❌ NeinZu viel Fluktuation
Performance-Optimierung⚠️ VielleichtBenchmark-Tests sind anders
Third-Party-Integration❌ NeinDeren Code, deren Tests

Teil 6: CI/CD – Automatisierung ist kein Luxus

Die Pipeline des Vertrauens

# .github/workflows/test.yml
name: Test Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      # Statische Analyse – Der erste Verteidigungswall
      - name: TypeScript Check
        run: npx tsc --noEmit

      - name: ESLint
        run: npm run lint

      # Unit & Integration Tests
      - name: Run Tests
        run: npm run test:ci

      # Coverage Report
      - name: Upload Coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

  e2e:
    needs: quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Run E2E Tests
        run: npm run test:e2e

Vitest CI-Konfiguration

// vitest.config.ts
import {defineConfig} from 'vitest/config';

export default defineConfig({
   test: {
      globals: true,
      environment: 'jsdom',
      setupFiles: ['./vitest.setup.ts'],
      coverage: {
         provider: 'v8',
         reporter: ['text', 'lcov', 'html'],
         exclude: [
            'node_modules/**',
            'src/**/*.test.ts',
            'src/**/*.d.ts',
            'src/mocks/**',
         ],
         thresholds: {
            // Nicht zu ambitioniert – wir sind realistisch
            lines: 70,
            functions: 70,
            branches: 60,
            statements: 70,
         },
      },
      // Für CI-Umgebungen
      ...(process.env.CI && {
         minThreads: 1,
         maxThreads: 2,
         reporters: ['verbose', 'junit'],
         outputFile: './test-results/junit.xml',
      }),
   },
});

Pre-Commit Hooks mit Husky + lint-staged

// package.json
{
  "scripts": {
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "vitest related --run"
    ]
  }
}

Teil 7: Alternativen und Ergänzungen

API-Mocking Alternativen

ToolAnwendungsfallProsCons
MSWFrontend + Node.jsNetzwerk-Level, ein Mock überallLernkurve
NockNode.js BackendEinfach, etabliertNur Node.js
MockoonDesktop GUIVisuell, OpenAPI-ImportSeparater Server
WireMockEnterprise, JavaMächtig, Contract TestingJava-lastig
Mirage.jsFrontend mit Data LayerEigene DB-SimulationKomplexer

Test-Runner Alternativen

ToolStärkeBeste für
VitestGeschwindigkeit, Vite-IntegrationModerne Vite-Projekte
JestÖkosystem, DokumentationLegacy, Create React App
Playwright TestE2E + UnitWenn Playwright schon da ist

Contract Testing

Für Microservices und API-First-Development:

// Mit Pact
import {Pact} from '@pact-foundation/pact';

const provider = new Pact({
   consumer: 'Frontend',
   provider: 'UserService',
});

describe('User API Contract', () => {
   beforeAll(() => provider.setup());
   afterAll(() => provider.finalize());

   it('erwartet einen User mit id und name', async () => {
      await provider.addInteraction({
         state: 'ein User existiert',
         uponReceiving: 'eine Anfrage nach User 1',
         withRequest: {
            method: 'GET',
            path: '/users/1',
         },
         willRespondWith: {
            status: 200,
            body: {
               id: 1,
               name: Matchers.string('Alice'),
            },
         },
      });

      const user = await fetchUser(1);
      expect(user.id).toBe(1);
   });
});

Fazit: Die Kunst des pragmatischen Testens

Die wichtigsten Takeaways

  1. Vitest + MSW = Das Dream-Team für moderne TypeScript/Vite-Projekte
  2. Integration > Unit für die meisten Frontend-Szenarien
  3. Teste Verhalten, nicht Implementierung
  4. 80% Coverage ist besser als 100% Burnout
  5. Automatisiere alles in CI/CD – dein zukünftiges Ich wird dir danken

Der ultimative Test-Entscheidungsbaum

Soll ich das testen?
        │
        ▼
   Kann es kaputt gehen?
        │
   ┌────┴────┐
   │         │
  Nein       Ja
   │         │
   ▼         ▼
Nicht     Wenn es kaputt geht,
testen    ist es dann peinlich?
               │
          ┌────┴────┐
          │         │
         Nein       Ja
          │         │
          ▼         ▼
      Vielleicht   TESTEN!
      testen       (Jetzt!)

Weiterführende Ressourcen


Geschrieben mit Koffein, Verzweiflung und der stillen Hoffnung, dass dieser Code nie wieder angefasst werden muss.

Letzte Aktualisierung: November 2025