Zum Inhalt springen

JavaScript Testing komplett – Jest, Vitest & Playwright (2025)

· von

JavaScript Testing mit Jest, Vitest und Playwright

~18 Min. Lesezeit · Veröffentlicht am

💡 Warum Testing wichtig ist: Tests sind keine Zeitverschwendung – sie sind eine Investition. Ein gut getesteter Code spart später 10x mehr Zeit bei der Fehlersuche und gibt Sicherheit bei Refactorings. Studien zeigen: Teams mit hoher Test-Coverage deployen 2x häufiger und haben 50% weniger Production-Bugs.

Keine Tests zu schreiben ist wie ohne Helm Fahrrad fahren – geht meistens gut, bis es das nicht mehr tut. Dieser Guide zeigt dir, wie du JavaScript-Code professionell testest: Von einfachen Unit Tests über Integration Tests bis zu vollständigen E2E-Tests mit modernen Tools.

Die Testing-Pyramide verstehen

Die klassische Testing-Pyramide zeigt das optimale Verhältnis verschiedener Test-Arten:


            /\
           /E2E\          ← Wenige, teure Tests (10%)
          /------\
         /Integr. \       ← Mittlere Anzahl (30%)
        /----------\
       /Unit Tests  \     ← Viele, schnelle Tests (60%)
      /______________\
Test-ArtWas wird getestet?GeschwindigkeitKostenTool-Empfehlung
Unit TestsEinzelne Funktionen/KlassenSehr schnell (ms)NiedrigJest/Vitest
Integration TestsZusammenspiel von ModulenSchnell (Sekunden)MittelJest/Vitest + Testing Library
E2E TestsKomplette User JourneysLangsam (Minuten)HochPlaywright/Cypress

Jest vs. Vitest – Welches Framework wählen?

FeatureJestVitest
PerformanceGutExzellent (ESM-nativ)
Vite-IntegrationNeinPerfekt
TypeScriptVia Babel/ts-jestOut-of-the-box
CommunityRiesigWachsend
Watch Mode✅ (schneller)
Snapshot Testing
🎯 Für Einsteiger: Starte mit Jest – es hat die größere Community und mehr Tutorials. Wenn du bereits Vite nutzt, nimm direkt Vitest.

Unit Testing mit Jest – Die Basics

Installation und Setup

# Jest installieren
npm install --save-dev jest @types/jest

# Für TypeScript zusätzlich
npm install --save-dev ts-jest typescript

# package.json anpassen
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Jest Konfiguration (jest.config.js)

module.exports = {
  // Test-Umgebung
  testEnvironment: 'node', // oder 'jsdom' für Browser-Code
  
  // Coverage-Settings
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  
  // Dateien die getestet werden
  testMatch: [
    '**/__tests__/**/*.[jt]s?(x)',
    '**/?(*.)+(spec|test).[jt]s?(x)'
  ],
  
  // Module transformieren
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  
  // Setup-Dateien
  setupFilesAfterEnv: ['/tests/setup.js'],
};

Dein erster Unit Test

// math.js - Die zu testende Funktion
export function calculateTotal(items) {
  if (!Array.isArray(items)) {
    throw new Error('Items must be an array');
  }
  
  return items.reduce((sum, item) => {
    const price = item.price || 0;
    const quantity = item.quantity || 1;
    const discount = item.discount || 0;
    
    return sum + (price * quantity * (1 - discount));
  }, 0);
}

// math.test.js - Der Test
import { calculateTotal } from './math';

describe('calculateTotal', () => {
  it('should calculate total for simple items', () => {
    const items = [
      { price: 10, quantity: 2 },
      { price: 5, quantity: 1 }
    ];
    
    expect(calculateTotal(items)).toBe(25);
  });
  
  it('should apply discounts correctly', () => {
    const items = [
      { price: 100, quantity: 1, discount: 0.2 } // 20% Rabatt
    ];
    
    expect(calculateTotal(items)).toBe(80);
  });
  
  it('should handle empty arrays', () => {
    expect(calculateTotal([])).toBe(0);
  });
  
  it('should throw error for non-arrays', () => {
    expect(() => calculateTotal('not an array')).toThrow('Items must be an array');
  });
  
  it('should handle missing properties with defaults', () => {
    const items = [
      { price: 10 }, // quantity defaults to 1
      { quantity: 3 } // price defaults to 0
    ];
    
    expect(calculateTotal(items)).toBe(10);
  });
});

Testing mit Vitest – Der moderne Ansatz

# Vitest installieren
npm install --save-dev vitest

# Mit UI (optional)
npm install --save-dev @vitest/ui

Vitest Konfiguration (vite.config.js)

import { defineConfig } from 'vite';

export default defineConfig({
  test: {
    // Jest-kompatible Globals
    globals: true,
    
    // Browser-ähnliche Umgebung
    environment: 'happy-dom',
    
    // Coverage mit c8
    coverage: {
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'tests/',
      ],
    },
    
    // Setup-Files
    setupFiles: './tests/setup.ts',
    
    // Reporter
    reporters: ['verbose'],
    
    // Watch ignorieren
    watchExclude: ['node_modules', 'dist']
  },
});

Fortgeschrittenes Testing: Mocks und Spies

// api.js - API-Client
export class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }
  
  async fetchUser(id) {
    const response = await fetch(`${this.baseURL}/users/${id}`);
    if (!response.ok) {
      throw new Error(`User ${id} not found`);
    }
    return response.json();
  }
}

// userService.js - Service der getestet wird
import { ApiClient } from './api';

export class UserService {
  constructor(apiClient) {
    this.apiClient = apiClient;
  }
  
  async getUserWithPosts(userId) {
    const user = await this.apiClient.fetchUser(userId);
    const posts = await this.apiClient.fetchPosts(userId);
    
    return {
      ...user,
      posts,
      postCount: posts.length
    };
  }
}

// userService.test.js - Test mit Mocks
import { describe, it, expect, vi } from 'vitest';
import { UserService } from './userService';

describe('UserService', () => {
  it('should fetch user with posts', async () => {
    // Mock ApiClient
    const mockApiClient = {
      fetchUser: vi.fn().mockResolvedValue({
        id: 1,
        name: 'John Doe',
        email: 'john@example.com'
      }),
      fetchPosts: vi.fn().mockResolvedValue([
        { id: 1, title: 'Post 1' },
        { id: 2, title: 'Post 2' }
      ])
    };
    
    const userService = new UserService(mockApiClient);
    const result = await userService.getUserWithPosts(1);
    
    // Assertions
    expect(result).toEqual({
      id: 1,
      name: 'John Doe',
      email: 'john@example.com',
      posts: [
        { id: 1, title: 'Post 1' },
        { id: 2, title: 'Post 2' }
      ],
      postCount: 2
    });
    
    // Verify mock calls
    expect(mockApiClient.fetchUser).toHaveBeenCalledWith(1);
    expect(mockApiClient.fetchPosts).toHaveBeenCalledWith(1);
    expect(mockApiClient.fetchUser).toHaveBeenCalledTimes(1);
  });
  
  it('should handle API errors', async () => {
    const mockApiClient = {
      fetchUser: vi.fn().mockRejectedValue(new Error('Network error'))
    };
    
    const userService = new UserService(mockApiClient);
    
    await expect(userService.getUserWithPosts(1))
      .rejects.toThrow('Network error');
  });
});

DOM Testing mit Testing Library

# Installation
npm install --save-dev @testing-library/dom @testing-library/jest-dom
// todoList.js - DOM Komponente
export function createTodoList(container) {
  const todos = [];
  
  function render() {
    container.innerHTML = `
      

Todo List (${todos.filter(t => !t.done).length} left)

    ${todos.map((todo, index) => `
  • ${todo.text}
  • `).join('')}
`; attachEventListeners(); } function attachEventListeners() { const input = container.querySelector('#todo-input'); const addBtn = container.querySelector('#add-btn'); addBtn?.addEventListener('click', () => { if (input.value.trim()) { todos.push({ text: input.value, done: false }); input.value = ''; render(); } }); container.querySelectorAll('input[type="checkbox"]').forEach((cb, index) => { cb.addEventListener('change', () => { todos[index].done = cb.checked; render(); }); }); } render(); return { todos, render }; } // todoList.test.js - DOM Tests import { screen, fireEvent } from '@testing-library/dom'; import '@testing-library/jest-dom'; import { createTodoList } from './todoList'; describe('TodoList', () => { let container; beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); }); afterEach(() => { document.body.removeChild(container); }); it('should add a new todo', () => { createTodoList(container); const input = container.querySelector('#todo-input'); const addBtn = container.querySelector('#add-btn'); // Neues Todo hinzufügen fireEvent.change(input, { target: { value: 'Write tests' } }); fireEvent.click(addBtn); // Prüfen ob Todo erscheint expect(container.querySelector('li span')).toHaveTextContent('Write tests'); expect(input.value).toBe(''); }); it('should mark todo as done', () => { const { todos } = createTodoList(container); // Todo hinzufügen const input = container.querySelector('#todo-input'); const addBtn = container.querySelector('#add-btn'); fireEvent.change(input, { target: { value: 'Test task' } }); fireEvent.click(addBtn); // Checkbox anklicken const checkbox = container.querySelector('input[type="checkbox"]'); fireEvent.click(checkbox); expect(todos[0].done).toBe(true); expect(container.querySelector('li')).toHaveClass('done'); }); it('should show correct count of remaining todos', () => { createTodoList(container); const input = container.querySelector('#todo-input'); const addBtn = container.querySelector('#add-btn'); // 2 Todos hinzufügen fireEvent.change(input, { target: { value: 'Task 1' } }); fireEvent.click(addBtn); fireEvent.change(input, { target: { value: 'Task 2' } }); fireEvent.click(addBtn); expect(container.querySelector('h2')).toHaveTextContent('Todo List (2 left)'); // Einen als done markieren const firstCheckbox = container.querySelector('input[type="checkbox"]'); fireEvent.click(firstCheckbox); expect(container.querySelector('h2')).toHaveTextContent('Todo List (1 left)'); }); });

E2E Testing mit Playwright

Playwright ist das modernste E2E-Testing-Tool mit Support für alle Browser und eingebauter Parallelisierung:

# Playwright installieren
npm init playwright@latest

# Oder manuell
npm install --save-dev @playwright/test

# Browser installieren
npx playwright install

Playwright Konfiguration (playwright.config.js)

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // Test-Verzeichnis
  testDir: './e2e',
  
  // Timeouts
  timeout: 30 * 1000,
  expect: {
    timeout: 5000
  },
  
  // Parallel ausführen
  fullyParallel: true,
  workers: process.env.CI ? 1 : undefined,
  
  // Reporter
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results.json' }]
  ],
  
  // Shared settings
  use: {
    // Base URL für relative URLs
    baseURL: 'http://localhost:3000',
    
    // Screenshots bei Fehlern
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    
    // Browser-Context
    actionTimeout: 0,
    trace: 'on-first-retry',
  },
  
  // Browser-Konfigurationen
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    // Mobile Tests
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],
  
  // Dev-Server automatisch starten
  webServer: {
    command: 'npm run dev',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

E2E Test Beispiel

// e2e/shopping-cart.spec.js
import { test, expect } from '@playwright/test';

test.describe('Shopping Cart', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
  });
  
  test('complete purchase flow', async ({ page }) => {
    // 1. Produkt zur Cart hinzufügen
    await page.click('[data-testid="product-1"]');
    await page.click('button:has-text("Add to Cart")');
    
    // Cart Badge prüfen
    await expect(page.locator('[data-testid="cart-badge"]'))
      .toHaveText('1');
    
    // 2. Zur Checkout gehen
    await page.click('[data-testid="cart-icon"]');
    await expect(page).toHaveURL('/cart');
    
    // Produkt in Cart sichtbar
    await expect(page.locator('h3'))
      .toContainText('Product 1');
    
    // 3. Checkout starten
    await page.click('button:has-text("Proceed to Checkout")');
    
    // 4. Formular ausfüllen
    await page.fill('input[name="email"]', 'test@example.com');
    await page.fill('input[name="name"]', 'John Doe');
    await page.fill('input[name="address"]', '123 Test St');
    await page.fill('input[name="city"]', 'Test City');
    await page.fill('input[name="zip"]', '12345');
    
    // 5. Payment Info
    await page.fill('input[name="cardNumber"]', '4242424242424242');
    await page.fill('input[name="expiry"]', '12/25');
    await page.fill('input[name="cvv"]', '123');
    
    // 6. Order abschließen
    await page.click('button:has-text("Place Order")');
    
    // Success-Message prüfen
    await expect(page.locator('.success-message'))
      .toContainText('Order placed successfully');
    
    // Order-Number vorhanden
    const orderNumber = await page.locator('[data-testid="order-number"]').textContent();
    expect(orderNumber).toMatch(/^ORDER-\d{6}$/);
  });
  
  test('should handle network errors gracefully', async ({ page, context }) => {
    // API-Calls abfangen
    await context.route('**/api/checkout', route => {
      route.abort('failed');
    });
    
    // Produkt hinzufügen und checkout versuchen
    await page.click('[data-testid="product-1"]');
    await page.click('button:has-text("Add to Cart")');
    await page.click('[data-testid="cart-icon"]');
    await page.click('button:has-text("Proceed to Checkout")');
    
    // Error-Message sollte erscheinen
    await expect(page.locator('.error-message'))
      .toContainText('Network error. Please try again.');
  });
  
  test('responsive design on mobile', async ({ page, browserName }) => {
    // Viewport auf Mobile setzen
    await page.setViewportSize({ width: 375, height: 667 });
    
    // Mobile Menu testen
    await expect(page.locator('.desktop-nav')).not.toBeVisible();
    await page.click('[data-testid="mobile-menu-toggle"]');
    await expect(page.locator('.mobile-nav')).toBeVisible();
    
    // Navigation funktioniert
    await page.click('a:has-text("Products")');
    await expect(page).toHaveURL('/products');
  });
});

Test-Driven Development (TDD) in der Praxis

TDD folgt dem Red-Green-Refactor Zyklus:

Der TDD-Zyklus:
1. RED - Test schreiben der fehlschlägt
2. GREEN - Minimalen Code schreiben damit Test passed
3. REFACTOR - Code verbessern ohne Tests zu brechen
// TDD Beispiel: Password Validator

// Schritt 1: RED - Test zuerst
describe('PasswordValidator', () => {
  it('should reject passwords shorter than 8 characters', () => {
    expect(validatePassword('short')).toBe(false);
  });
});

// Schritt 2: GREEN - Minimale Implementation
function validatePassword(password) {
  return password.length >= 8;
}

// Schritt 3: Weitere Tests hinzufügen
it('should require at least one uppercase letter', () => {
  expect(validatePassword('lowercase123')).toBe(false);
  expect(validatePassword('Uppercase123')).toBe(true);
});

// Schritt 4: Implementation erweitern
function validatePassword(password) {
  if (password.length > 8) return false;
  if (!/[A-Z]/.test(password)) return false;
  return true;
}

// Schritt 5: Mehr Requirements
it('should require at least one number', () => {
  expect(validatePassword('NoNumbers')).toBe(false);
});

it('should require at least one special character', () => {
  expect(validatePassword('NoSpecial1')).toBe(false);
  expect(validatePassword('Valid@Pass1')).toBe(true);
});

// Finale Implementation
function validatePassword(password) {
  const rules = [
    { regex: /.{8,}/, message: 'At least 8 characters' },
    { regex: /[A-Z]/, message: 'One uppercase letter' },
    { regex: /[a-z]/, message: 'One lowercase letter' },
    { regex: /\d/, message: 'One number' },
    { regex: /[@$!%*?&#]/, message: 'One special character' }
  ];
  
  const errors = rules
    .filter(rule => !rule.regex.test(password))
    .map(rule => rule.message);
  
  return {
    valid: errors.length === 0,
    errors
  };
}

// Tests anpassen für neue Struktur
it('should return detailed error messages', () => {
  const result = validatePassword('weak');
  expect(result.valid).toBe(false);
  expect(result.errors).toContain('At least 8 characters');
  expect(result.errors).toContain('One uppercase letter');
});

CI/CD Integration mit GitHub Actions

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

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [18.x, 20.x]
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run Unit Tests
      run: npm run test:unit
    
    - name: Run Integration Tests
      run: npm run test:integration
    
    - name: Generate Coverage Report
      run: npm run test:coverage
    
    - name: Upload Coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/coverage-final.json
        flags: unittests
        name: codecov-umbrella
    
    # E2E Tests
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    
    - name: Run E2E Tests
      run: npm run test:e2e
    
    - name: Upload Playwright Report
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30
    
    # Performance Budget Check
    - name: Check Bundle Size
      run: npm run size-limit
    
    # Notify on failure
    - name: Slack Notification
      if: failure()
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        text: 'Tests failed on ${{ github.ref }}'
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}

Coverage Reports verstehen

Coverage-Metriken erklärt:
Line Coverage: Prozent der ausgeführten Code-Zeilen
Branch Coverage: Prozent der durchlaufenen if/else-Zweige
Function Coverage: Prozent der aufgerufenen Funktionen
Statement Coverage: Prozent der ausgeführten Statements

Ziel: 80% Coverage ist ein guter Wert. 100% ist meist nicht sinnvoll.
// Coverage-Thresholds in package.json
{
  "jest": {
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      },
      "./src/components/": {
        "branches": 90,  // Kritische Components höher
        "functions": 90,
        "lines": 90
      }
    }
  }
}

Performance Testing

// performance.test.js - Performance-Tests mit Vitest
import { describe, it, expect } from 'vitest';
import { processLargeDataset } from './dataProcessor';

describe('Performance Tests', () => {
  it('should process 10000 items under 100ms', () => {
    const items = Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      value: Math.random() * 1000
    }));
    
    const start = performance.now();
    processLargeDataset(items);
    const duration = performance.now() - start;
    
    expect(duration).toBeLessThan(100);
  });
  
  it('should not exceed memory limit', () => {
    const initialMemory = process.memoryUsage().heapUsed;
    
    // Heavy operation
    const result = processLargeDataset(
      Array.from({ length: 100000 }, () => ({ data: 'x'.repeat(1000) }))
    );
    
    const memoryIncrease = process.memoryUsage().heapUsed - initialMemory;
    const memoryIncreaseMB = memoryIncrease / 1024 / 1024;
    
    expect(memoryIncreaseMB).toBeLessThan(50); // Max 50MB increase
  });
});

Testing Best Practices

✅ Arrange-Act-Assert
Strukturiere Tests in 3 klare Phasen
✅ Ein Assertion pro Test
Tests sollten eine Sache testen
✅ Descriptive Namen
"should calculate tax for international orders"
✅ Test Isolation
Tests dürfen nicht voneinander abhängen
✅ Keine Logik in Tests
Keine Loops oder Conditions in Tests
✅ Mock External Dependencies
APIs, Databases, File System mocken
✅ Fast Feedback
Unit Tests > 10ms, Integration > 100ms
✅ Deterministic
Gleicher Input = Gleicher Output, immer

Testing Anti-Patterns vermeiden

❌ Test-Code Duplication: Nutze beforeEach/afterEach und Helper-Funktionen
❌ Testing Implementation Details: Teste Behavior, nicht wie etwas implementiert ist
❌ Große Test-Setups: Wenn Setup < Test-Code, refactore den Code
❌ Flaky Tests: Tests die manchmal failen sind schlimmer als keine Tests

Advanced: Mutation Testing

Mutation Testing prüft die Qualität deiner Tests indem es absichtlich Bugs einführt:

# Stryker Mutation Testing
npm install --save-dev @stryker-mutator/core

# Konfiguration
npx stryker init
// stryker.conf.js
module.exports = {
  packageManager: 'npm',
  reporters: ['html', 'progress', 'dashboard'],
  testRunner: 'jest',
  coverageAnalysis: 'perTest',
  mutate: ['src/**/*.js', '!src/**/*.test.js'],
  thresholds: { high: 80, low: 60, break: 50 }
};
🚀 Pro-Tipp: Mutation Testing zeigt dir, ob deine Tests wirklich Bugs finden würden. Ein Mutation Score von 75% ist exzellent.

Testing Cheatsheet

// === JEST/VITEST MATCHER ===
expect(value).toBe(4);                  // Exakter Vergleich
expect(value).toEqual({id: 1});         // Deep equality
expect(value).toBeNull();               // Ist null
expect(value).toBeDefined();            // Ist definiert
expect(value).toBeTruthy();             // Truthy value
expect(value).toBeFalsy();              // Falsy value

// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeCloseTo(0.3);         // Floating point

// Strings
expect('team').toMatch(/I/);
expect('Christoph').toMatch('stop');

// Arrays
expect(['Alice', 'Bob']).toContain('Alice');
expect(new Set(['Alice', 'Bob'])).toContain('Alice');
expect(['Alice', 'Bob', 'Eve']).toHaveLength(3);

// Exceptions
expect(() => compileCode()).toThrow();
expect(() => compileCode()).toThrow(Error);
expect(() => compileCode()).toThrow('you are using the wrong JDK');

// Async
await expect(fetchData()).resolves.toBe('data');
await expect(fetchData()).rejects.toThrow('error');

// === MOCKING ===
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
mockFn.mockReturnValueOnce(10);
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('Async error'));

// Spy on existing
const spy = jest.spyOn(object, 'method');
spy.mockImplementation(() => 42);

// Module mocks
jest.mock('./module');
jest.unmock('./module');

// === PLAYWRIGHT SELECTORS ===
page.locator('button');                        // CSS
page.locator('text=Submit');                   // Text
page.locator('[data-testid="submit"]');       // Test ID
page.locator('button:has-text("Submit")');    // Combined
page.locator('li >> text=Item');              // Nested

// Actions
await page.click('button');
await page.fill('input', 'text');
await page.check('input[type="checkbox"]');
await page.selectOption('select', 'value');
await page.hover('button');

// Assertions
await expect(page).toHaveTitle('Page Title');
await expect(page).toHaveURL('https://example.com');
await expect(locator).toBeVisible();
await expect(locator).toBeEnabled();
await expect(locator).toHaveText('Expected text');
await expect(locator).toHaveValue('input value');
await expect(locator).toHaveCount(3);

Fazit: Testing als Investition

Tests sind keine Zeitverschwendung – sie sind eine Investition in die Zukunft deines Codes:

✅ Was du gewinnst:
Vertrauen: Refactoring ohne Angst
Dokumentation: Tests zeigen wie Code funktioniert
Schnellere Entwicklung: Bugs werden früh gefunden
Bessere Architektur: Testbarer Code ist besserer Code
Team-Velocity: Neue Entwickler verstehen Code schneller

„Code ohne Tests ist schlechtes Design."

– Michael Feathers, Working Effectively with Legacy Code
🎯 Starte heute:
1. Wähle ein Framework (Jest für Anfänger, Vitest für Vite-Projekte)
2. Schreibe einen Test für deine wichtigste Funktion
3. Füge bei jedem Bug einen Test hinzu
4. Setze dir ein Coverage-Ziel (z.B. 70%)
5. Integriere Tests in deine CI/CD Pipeline

Mehr zu CI/CD findest du in meinem Artikel Git Advanced – Workflows für Teams.
Für TypeScript-Testing lies TypeScript für JavaScript-Entwickler.
Build-Integration erkläre ich in Build-Pipeline mit Node.