Design wechseln

Next.js E2E-Tests: Praxisleitfaden für automatisierte Tests mit Playwright

Easton editorial illustration: rendering-mode selector

Auf dem Bug-Ticket blinkt ein rotes „Dringend“ – der Payment-Flow ist wieder kaputt. In Staging alles grün, in Production nicht.

Letzte Woche vor dem Release: über dreißig Seiten manuell geklickt, ein Dutzend Formulare ausgefüllt, drei Browser durch – und trotzdem der Button übersehen, der nur auf Mobile ganz unten sichtbar wird. Die PM-Nachricht: „Nutzer sagen, Gutscheine gehen nicht.“

Manuelles Testen hält nicht mehr mit.

Ich habe Cypress, Selenium, Puppeteer und Playwright verglichen – am Ende Playwright: Multi-Browser, Setup einfacher als bei Cypress. In der ersten Woche fünf Bugs, die manuell nie auffielen – Firefox-only-CSS, Race Conditions bei async APIs.

Am Anfang viele Stolpersteine: Config mehrfach überarbeitet, Tests zweimal neu geschrieben. Heute läuft alles durch, CI/CD vollautomatisch, jeder Commit triggert Tests – Production-Bugs deutlich seltener.

Warum Playwright (vs. Cypress)

Cypress kennt jeder: einfaches Setup, gute Docs, aktive Community. Warum trotzdem Playwright?

Drei Punkte haben mich umgestimmt:

Schwache Multi-Browser-Unterstützung. Firefox und Safari bei Cypress waren lange halbgar – praktisch läuft vieles nur in Chromium. Einmal Payment-Seite in Chrome perfekt, in Safari White Screen – wegen einer CSS-Eigenschaft, die Safari nicht mag. Playwright unterstützt Chromium, Firefox und WebKit nativ; eine Suite deckt die gängigen Browser ab.

Geschwindigkeit. Playwright parallelisiert stark. Cypress oft seriell – 50 Cases in über zehn Minuten; Playwright mit 8 Workern in etwa fünf. In CI zählt jede Minute.

API-Design. Von Cypress zu Playwright fühlt sich async/await zuerst fremd an – Cypress’ Chaining ist angenehm. Nach einer Weile passt es besser zu modernem JavaScript und zum Stil von Next.js Server Components.

Cypress ist nicht schlecht. Nur Chrome, wenig Test-Erfahrung im Team → Cypress ist oft der schnellere Einstieg. Debugging und Time Travel sind exzellent.

Für mich passte Playwright besser:

  • Cross-Browser nötig
  • Next.js/React-Erfahrung, async/await vertraut
  • schnelles Feedback in CI
  • API Routes und SSR-Seiten testen

Kein absolutes „besser/schlechter“ – kontextabhängig. Kleines Projekt, wenig Test-Kultur: Cypress zum schnellen Start. Größerer Stack, langfristige Automation: Playwright.

Next.js + Playwright: Konfiguration in der Praxis

Installation in drei Schritten:

npm init playwright@latest
# oder mit pnpm
pnpm create playwright

Beim Setup empfehle ich:

  • TypeScript? Yes (Typen sparen Fehler)
  • Testverzeichnis? tests (Standard reicht)
  • GitHub Actions? Yes (für CI/CD)

Danach typischerweise:

your-nextjs-project/
├── tests/               # Testfälle
│   └── example.spec.ts
├── playwright.config.ts # Playwright-Konfiguration
└── .github/
    └── workflows/
        └── playwright.yml  # CI-Konfiguration

Konfiguration: typische Fallstricke

Die Standard-playwright.config.ts passt für normale Web-Apps – bei Next.js sollten Sie anpassen. Nach einem halben Jahr ist das meine stabile Variante:

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

export default defineConfig({
  // Testverzeichnis
  testDir: './tests',

  // Globales Timeout: 30 Sekunden pro Test
  timeout: 30 * 1000,

  // Expect-Timeout: 5 Sekunden für Element-Suche
  expect: {
    timeout: 5000,
  },

  // Retries bei Fehlschlag (in CI empfohlen)
  retries: process.env.CI ? 2 : 0,

  // Parallele Worker (8 Kerne → 4 lokal)
  workers: process.env.CI ? 2 : 4,

  // Reporter
  reporter: [
    ['html'],                    // HTML-Report
    ['list'],                    // Terminal-Liste
    process.env.CI ? ['github'] : ['list'], // GitHub-Format in CI
  ],

  // Next.js Dev-Server starten
  webServer: {
    command: 'npm run dev',
    port: 3000,
    timeout: 120 * 1000,         // Cold Start: genug Zeit für Kompilierung
    reuseExistingServer: !process.env.CI, // Lokal Server wiederverwenden
  },

  // Projekte (Multi-Browser)
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    // Mobile (optional)
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],

  // Globale Einstellungen
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',      // Trace bei Retry für Debugging
    screenshot: 'only-on-failure', // Screenshot bei Fehler
    video: 'retain-on-failure',   // Video bei Fehler behalten
  },
});

Häufige Stolpersteine

  1. webServer.timeout großzügig setzen. Anfangs 30 Sekunden – Next.js Cold Start kompiliert, Timeout. 120 Sekunden sind stabil.

  2. Lokal reuseExistingServer: true. Sonst startet Next.js bei jedem Testlauf neu – nervt.

  3. Worker-Anzahl begrenzen. Einmal so viele Worker wie CPU-Kerne – Rechner hing. Jetzt etwa die Hälfte der Kerne: schnell und stabil.

  4. Mobile Tests optional. Bei responsivem Next.js fängt Mobile Chrome mobile Bugs – verdoppelt aber oft die Laufzeit.

Config steht? Beispieltests starten:

npx playwright test

Grünes passed → Umgebung passt. Dann echte Testfälle schreiben.

Best Practices für UI-Tests (Page Object Model)

Am Anfang alles in einer Datei – Login-Test über 100 Zeilen, page.locator, page.fill, page.click überall. Selektor geändert → zwölf Dateien anfassen. Erschöpfend.

Page Object Model (POM) macht den Code deutlich lesbarer: Seitenaktionen in eine Klasse, Tests rufen nur Methoden auf.

Ohne POM (Anti-Pattern)

// tests/login.spec.ts
import { test, expect } from '@playwright/test';

test('用户登录', async ({ page }) => {
  await page.goto('/login');

  // 直接操作元素,代码重复
  await page.locator('input[name="email"]').fill('[email protected]');
  await page.locator('input[name="password"]').fill('password123');
  await page.locator('button[type="submit"]').click();

  await expect(page.locator('h1')).toContainText('Dashboard');
});

test('登录失败提示', async ({ page }) => {
  await page.goto('/login');

  // 又来一遍同样的操作...
  await page.locator('input[name="email"]').fill('[email protected]');
  await page.locator('input[name="password"]').fill('wrongpass');
  await page.locator('button[type="submit"]').click();

  await expect(page.locator('.error')).toBeVisible();
});

Problem: ändert Design input[name="email"] zu input[id="email"], müssen alle Tests mit.

Mit POM (empfohlen)

Page Object anlegen:

// tests/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;
  readonly dashboardTitle: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.locator('input[name="email"]');
    this.passwordInput = page.locator('input[name="password"]');
    this.submitButton = page.locator('button[type="submit"]');
    this.errorMessage = page.locator('.error');
    this.dashboardTitle = page.locator('h1');
  }

  // 封装登录操作
  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  // 封装导航操作
  async goto() {
    await this.page.goto('/login');
  }

  // 封装验证逻辑
  async expectLoginSuccess() {
    await this.dashboardTitle.waitFor();
    await expect(this.dashboardTitle).toContainText('Dashboard');
  }

  async expectLoginError() {
    await expect(this.errorMessage).toBeVisible();
  }
}

Tests werden kurz:

// tests/login.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

test('用户登录', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('[email protected]', 'password123');
  await loginPage.expectLoginSuccess();
});

test('登录失败提示', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('[email protected]', 'wrongpass');
  await loginPage.expectLoginError();
});

Selektor-Änderung nur in LoginPage.ts. Tests lesen sich wie Prosa – auch für Neue im Team.

Verzeichnisstruktur im echten Projekt

tests/
├── pages/                  # Page Objects
│   ├── LoginPage.ts
│   ├── DashboardPage.ts
│   └── CheckoutPage.ts
├── fixtures/               # Testdaten und Helfer
│   └── testData.ts
├── auth.spec.ts           # Auth-Tests
├── checkout.spec.ts       # Checkout-Flow
└── dashboard.spec.ts      # Dashboard-Tests

Erfahrungen und Tipps

  1. Nicht over-engineeren. Nicht jede Seite braucht ein Page Object. Einmal getestet → direkt im Test reicht.

  2. Semantische Methodennamen. async fillLoginForm() schlägt async fillForm() – Ihr zukünftiges Ich dankt Ihnen.

  3. Warte-Logik ins Page Object. Playwright wartet smart, manchmal braucht es waitFor() – dort kapseln, Tests bleiben sauber.

  4. Testdaten zentral. Credentials in fixtures/testData.ts:

// tests/fixtures/testData.ts
export const testUsers = {
  validUser: {
    email: '[email protected]',
    password: 'password123'
  },
  invalidUser: {
    email: '[email protected]',
    password: 'wrongpass'
  }
};

Verwendung:

import { testUsers } from './fixtures/testData';

await loginPage.login(testUsers.validUser.email, testUsers.validUser.password);

Wartungskosten sinken spürbar. Workflow: Page Object → wenige Testzeilen → fertig.

E2E-Tests für API Routes

Next.js API Routes gehören zur App – also testen. Früher Postman per Hand, heute Playwright mit request, oft ohne Browser.

Basis-API-Tests

Beispiel: User-Liste abrufen:

// tests/api/users.spec.ts
import { test, expect } from '@playwright/test';

test.describe('用户 API 测试', () => {
  test('GET /api/users - 获取用户列表', async ({ request }) => {
    const response = await request.get('/api/users');

    // 验证状态码
    expect(response.status()).toBe(200);

    // 验证响应格式
    const users = await response.json();
    expect(Array.isArray(users)).toBeTruthy();
    expect(users.length).toBeGreaterThan(0);

    // 验证数据结构
    expect(users[0]).toHaveProperty('id');
    expect(users[0]).toHaveProperty('email');
    expect(users[0]).toHaveProperty('name');
  });

  test('POST /api/users - 创建用户', async ({ request }) => {
    const newUser = {
      email: '[email protected]',
      name: 'Test User',
      password: 'password123'
    };

    const response = await request.post('/api/users', {
      data: newUser
    });

    expect(response.status()).toBe(201);

    const createdUser = await response.json();
    expect(createdUser.email).toBe(newUser.email);
    expect(createdUser).not.toHaveProperty('password'); // 密码不应该返回
  });

  test('POST /api/users - 邮箱重复应返回错误', async ({ request }) => {
    const duplicateUser = {
      email: '[email protected]',
      name: 'Duplicate User',
      password: 'password123'
    };

    const response = await request.post('/api/users', {
      data: duplicateUser
    });

    expect(response.status()).toBe(400);

    const error = await response.json();
    expect(error.message).toContain('邮箱已存在');
  });
});

API-Tests mit Authentifizierung

Viele Endpoints brauchen Login – Token holen, Header setzen:

// tests/api/auth.spec.ts
import { test, expect } from '@playwright/test';

let authToken: string;

test.describe('需要认证的 API', () => {
  // 所有测试前先登录获取 token
  test.beforeAll(async ({ request }) => {
    const response = await request.post('/api/auth/login', {
      data: {
        email: '[email protected]',
        password: 'password123'
      }
    });

    const { token } = await response.json();
    authToken = token;
  });

  test('GET /api/profile - 获取用户资料', async ({ request }) => {
    const response = await request.get('/api/profile', {
      headers: {
        'Authorization': `Bearer ${authToken}`
      }
    });

    expect(response.status()).toBe(200);

    const profile = await response.json();
    expect(profile.email).toBe('[email protected]');
  });

  test('未登录访问应返回 401', async ({ request }) => {
    const response = await request.get('/api/profile');
    expect(response.status()).toBe(401);
  });
});

Gemischte Tests: UI + API

Stark kombiniert – z. B. Artikel veröffentlichen:

// tests/posts.spec.ts
import { test, expect } from '@playwright/test';

test('发布文章完整流程', async ({ page, request }) => {
  // 1. 先通过页面登录
  await page.goto('/login');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  // 2. 进入文章编辑页
  await page.goto('/posts/new');
  await page.fill('input[name="title"]', '测试文章标题');
  await page.fill('textarea[name="content"]', '这是测试内容');
  await page.click('button:has-text("发布")');

  // 3. 等待跳转到文章详情页
  await page.waitForURL(/\/posts\/\d+/);

  // 4. 通过 API 验证文章确实创建成功
  const url = page.url();
  const postId = url.split('/').pop();

  const response = await request.get(`/api/posts/${postId}`);
  expect(response.status()).toBe(200);

  const post = await response.json();
  expect(post.title).toBe('测试文章标题');
  expect(post.content).toBe('这是测试内容');
  expect(post.status).toBe('published');
});

Vorteil: UI und Backend zusammen. Einmal zeigte die Seite „Erfolg“, in der DB stand draft – Status-Update war falsch.

Praxis-Tipps

  1. Grenzfälle testen. Fehlende Parameter, falsche Typen, fehlende Rechte – nicht nur Happy Path.

  2. Testdaten aufräumen. API schreibt in die DB – afterAll oder dedizierte Test-DB:

test.afterAll(async ({ request }) => {
  await request.delete('/api/test/cleanup');
});
  1. Externe Dienste mocken. Payment/SMS in Tests nicht live aufrufen.

  2. Latenz prüfen. Optional Timing asserten:

const start = Date.now();
await request.get('/api/users');
const duration = Date.now() - start;

expect(duration).toBeLessThan(1000); // 接口响应应该在 1 秒内

UI + API decken oft ~90 % ab – den Rest fangen Unit-Tests.

GitHub Actions CI/CD

Tests geschrieben → CI anbinden. Jeder Push läuft automatisch – wie oft war man sicher, bis CI rot wurde.

npm init playwright legt oft schon .github/workflows/playwright.yml an – die Basis reicht selten produktiv.

Basis-CI

Standard-Workflow:

name: Playwright Tests

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

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - uses: actions/setup-node@v3
      with:
        node-version: 18

    - name: Install dependencies
      run: npm ci

    - name: Install Playwright Browsers
      run: npx playwright install --with-deps

    - name: Run Playwright tests
      run: npx playwright test

    - uses: actions/upload-artifact@v3
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

Schwächen: Browser jedes Mal neu, keine Test-DB, Report nur als Download.

Produktionsnahe Konfiguration

Mit Cache, DB und Report-Deploy:

name: E2E Tests

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

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest

    services:
      # 测试数据库(PostgreSQL)
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    env:
      DATABASE_URL: postgresql://test:test@localhost:5432/testdb
      NODE_ENV: test

    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: Cache Playwright browsers
      uses: actions/cache@v3
      with:
        path: ~/.cache/ms-playwright
        key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}

    - name: Install Playwright Browsers
      run: npx playwright install --with-deps chromium

    - name: Run database migrations
      run: npm run db:migrate

    - name: Run Playwright tests
      run: npx playwright test

    - name: Upload test results
      if: always()
      uses: actions/upload-artifact@v4
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

    # 如果是主分支,把报告部署到 GitHub Pages
    - name: Deploy report to GitHub Pages
      if: always() && github.ref == 'refs/heads/main'
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./playwright-report

Wichtige Punkte

  1. Service-Container: PostgreSQL für API-Tests – bei MySQL mysql:8 analog.

  2. Browser-Cache: actions/cache für ~/.cache/ms-playwright. In CI oft nur Chromium – drei Browser sind langsam.

  3. Migrationen vor Tests: z. B. in package.json:

{
  "scripts": {
    "db:migrate": "prisma migrate deploy"
  }
}
  1. Report auf GitHub Pages: Team sieht Ergebnisse im Browser.

Umgebungsvariablen

Secrets unter Settings → Secrets:

env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
  STRIPE_SECRET_KEY: ${{ secrets.STRIPE_TEST_KEY }}

Debugging bei CI-Fehlern

  1. Trace: trace: 'on-first-retry'npx playwright show-trace trace.zip replayt den Lauf.

  2. Screenshots/Videos bei Failure.

  3. Lokal CI simulieren mit act:

# 安装 act
brew install act  # macOS
# 或
choco install act  # Windows

# 运行 workflow
act -j test

Fallstricke

  1. Timeout sinnvoll. 30 Minuten und ein hängender Test verschwenden Runner-Zeit – 60 Minuten mit Monitoring langsamer Tests.

  2. Wenige Worker in CI. Zwei Worker reichen oft.

  3. Retries begrenzen. retries: 2 für Flakiness – echte Bugs löst Wiederholen nicht.

Mit CI steigt die Qualität spürbar: PRs brauchen grün vor Merge.

Abdeckung und Reports

Nach dem Lauf zählt das Ergebnis. Playwright-HTML-Reports sind übersichtlich.

HTML-Report

npx playwright show-report

Zeigt Status, Dauer, Screenshots, Videos, Trace. Trace Viewer ist Gold: Schritte, Requests, DOM, Console – präzise Fehlerstelle.

Funktionsabdeckung

E2E misst Funktions-, nicht Code-Coverage. Checkliste im Repo:

## 测试覆盖清单

### 用户认证
- [x] 登录(正常流程)
- [x] 登录失败(错误密码)
- [x] 注册
- [x] 找回密码
- [ ] 第三方登录(Google)

### 商品管理
- [x] 添加商品
- [x] 编辑商品
- [x] 删除商品
- [ ] 批量导入

### 订单流程
- [x] 加购物车
- [x] 结算
- [x] 支付(模拟环境)
- [ ] 退款流程

In tests/README.md pflegen – Lücken auf einen Blick.

Code-Coverage (optional)

Mit Istanbul/v8 möglich – selten nötig für E2E:

// playwright.config.ts
export default defineConfig({
  use: {
    // 启用代码覆盖率
    trace: 'on',
    // 注入覆盖率收集代码
    contextOptions: {
      recordVideo: {
        dir: 'test-results/videos'
      }
    }
  }
});

Unit-Tests decken Logik ab; E2E prüft, ob Flows für Nutzer funktionieren.

Custom Reporter

Slack/DingTalk-Benachrichtigung:

// my-reporter.ts
import { Reporter } from '@playwright/test/reporter';

class SlackReporter implements Reporter {
  onEnd(result) {
    const passed = result.suites.filter(s => s.ok).length;
    const failed = result.suites.length - passed;

    // 发送到 Slack
    fetch('https://hooks.slack.com/services/YOUR_WEBHOOK', {
      method: 'POST',
      body: JSON.stringify({
        text: `测试完成:${passed} 通过,${failed} 失败`
      })
    });
  }
}

export default SlackReporter;

Aktivieren:

// playwright.config.ts
export default defineConfig({
  reporter: [
    ['html'],
    ['./my-reporter.ts']
  ]
});

Playwright Test Runner visualisiert Traces. Pragmatisch: Pass-Rate und Laufzeit in CSV → Google Sheets.

Mein Workflow

  1. Lokal: Terminal; bei Failure --debug:

    npx playwright test --debug
  2. PR-Review: CI-HTML-Report, langsame Tests prüfen.

  3. Wöchentlich: Coverage-Checkliste aktualisieren.

Reports sind mehr als Zahlen – sie verbessern Prozess und Qualität.

Fazit

Vor einem halben Jahr manuell bis drei Uhr nachts – heute deutlich entspannter.

Playwright + Next.js wirkt wartungsarm: einmal sauber konfiguriert, CI auf jedem Commit, Releases mit mehr Vertrauen. Weniger Production-Bugs, weniger PM-Panik.

Wenn Sie noch manuell testen:

  1. Mit Kernflows starten – Login, Payment zuerst, nicht alles auf einmal.
  2. Page Object Model nutzen – kurzfristig mehr Aufwand, langfristig Gewinn.
  3. CI/CD – Automation ohne automatischen Lauf lohnt wenig.
  4. Keine 100 %-Jagd – Kern sichern, Rest pragmatisch.

E2E ist auch Team-Kultur: gemeinsame Qualitätslatte, Tests als lebendige Dokumentation, planbare Releases.

Ich bin oft um halb sechs fertig – Zeit fürs Fitnessstudio, die Jahreskarte war fast verfallen.

Fangen Sie mit Tests an – Ihr späteres Ich wird dankbar sein.

FAQ

Playwright oder Cypress – was soll ich wählen?
Das hängt von Ihrem Projekt ab:

• Playwright: Cross-Browser (Chromium/Firefox/WebKit), schnelle Parallelisierung (8 Worker etwa 3× schneller als Cypress), async/await-Syntax, geeignet für mittlere und große Projekte
• Cypress: Chrome-fokussiert, starkes Time-Travel-Debugging, große Community, einsteigerfreundlich

Nur Chrome und wenig Test-Erfahrung im Team → Cypress. Cross-Browser, schnelles CI, React/Next.js-Erfahrung → Playwright.
Muss ich Page Object Model unbedingt verwenden?
Nicht zwingend, aber sehr empfehlenswert.

Bei kleinen Projekten (unter 10 Testfällen) oder einmalig getesteten Seiten reicht direkter Code in den Tests. Aber wenn:
• mehrere Tests dieselbe Seite bedienen
• mehrere Personen Tests pflegen
• das Projekt langfristig wächst

dann spart POM viel Ärger. Ein geänderter Selektor zeigt den Wert: ohne POM 10+ Dateien anfassen, mit POM nur eine.
Tests in CI laufen ständig in Timeout – was tun?
Typische Ursachen und Lösungen:

• webServer.timeout zu kurz: auf 120 Sekunden setzen (Next.js Cold Start kompiliert)
• zu viele Worker: in CI 2–4 reichen meist
• fehlerhafte Tests: Trace-Datei analysieren – langsame Requests oder Element-Timeouts?
• langsame Browser-Installation: actions/cache für Browser-Dateien

Tipp: in CI nur Chromium, lokal alle Browser – deutlich schneller.
Wie verwalte ich Testdaten? Muss ich die Datenbank jedes Mal manuell leeren?
Drei Ansätze:

• Separate Test-Datenbank: nur für Tests, regelmäßig leeren, Dev-Umgebung unberührt
• Nach jedem Test aufräumen: Cleanup in test.afterAll() – Lücken möglich
• Docker-Container: frische DB pro Lauf, danach zerstören (sauber, aber langsamer)

Praxis: Kombination – CI mit Docker-DB, lokal separate Test-DB plus afterAll-Cleanup.
Soll ich Drittanbieter-APIs in Tests mocken?
Ja, aus drei Gründen:

• Kosten: echte Payment-/SMS-Calls kosten pro Lauf
• Geschwindigkeit: langsame Drittanbieter bremsen die Suite
• Stabilität: Ausfall externer Dienste darf Tests nicht killen

Playwright kann Requests abfangen:
await page.route('**/api/payment', route => route.fulfill({ status: 200, body: '{"success": true}' }));

Alternativ in Next.js API Routes per Umgebungsvariable Mock-Daten zurückgeben.
Wie viel Testabdeckung ist „gut genug“?
Bei E2E zählt Funktionsabdeckung, nicht Code-Coverage.

Prioritäten:
• Kernflows (Login, Payment, Bestellung): 100 %
• häufig genutzte Features (Produkte, Warenkorb): über 80 %
• seltene Flows (Passwort-Reset, Refund): über 50 %
• Randfeatures (Theme, Sprache): optional

Nicht 100 % jagen – Kernflows sicher, Rest pragmatisch. 100 % Kern + ~60 % gesamt fängt oft ~90 % der Bugs.
Brauche ich nach Playwright-E2E noch Unit-Tests?
Ja, sie ergänzen sich:

• E2E (Playwright): User-Flows, Integration, langsam aber breit
• Unit (Jest/Vitest): Logik, Grenzfälle, Fehlerbehandlung – schnell, lokal

Faustregel: ~70 % Unit, ~30 % E2E. Utils, Hooks, Komponentenlogik per Unit; komplette User-Journeys per E2E.

Unit findet die Stelle, E2E beweist, dass es für Nutzer funktioniert.

12 Min. Lesezeit · Veröffentlicht am: 7. Jan. 2026 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog