branch-coexist: 3 neue decks 223015b-2026 / 223015c-2026 / dhbw-2026 daneben main
per user-request einen branch der main-content UND rewrite-content trägt — saubere separierung statt symlink/merge-conflict. struktur: - slides/<course>/ → main-content restored, eigene assets (kein new-demo-leak) - slides/<course>-2026/ → rewrite-content, eigene assets (komplett kopiert + new demos) Makefile: COURSES += 223015b-2026 223015c-2026 dhbw-2026 mit eigenen NAME/KAPITEL/DEPLOY/KLAUSUR-konfig. Klausur für b+c=1, dhbw=leer. Deploy-paths /hdm/223015b-2026 etc. make build durchlaufen für alle 6 decks, 0 fehler. dedup: 223015c+dhbw assets identisch in beiden ordnern (kein neu-demos auf b ergänzt). 223015b 86 → 120 files in -2026 (= +34 neue eroeffner/usb-c-drei-achsen/container-vs-codec/etc).
This commit is contained in:
1 parent
6209f9f3a0
commit
351e3dfc23
467 files changed
+32391
-4701
No files matched your search
@@ -0,0 +1,383 @@
|
||||
---
|
||||
marp: true
|
||||
theme: gaia
|
||||
paginate: true
|
||||
backgroundColor: #fff
|
||||
header: "Web Engineering – DHBW Stuttgart"
|
||||
footer: "Michael Czechowski – SoSe 2026"
|
||||
title: Testing
|
||||
---
|
||||
|
||||
<style>
|
||||
:root { --color-foreground: #1a1a2e; --color-highlight: #d63384; }
|
||||
section.invert { --color-foreground: #fff; }
|
||||
section { font-size: 1.4rem; }
|
||||
h1 { color: #a02060; }
|
||||
section.invert h1 { color: #fff; }
|
||||
h2 { color: #1f2937; }
|
||||
pre { background: #0f0f23; color: #f48fb1; border-radius: 8px; border-left: 3px solid #d63384; }
|
||||
pre code { background: transparent; color: inherit; }
|
||||
code { background: #1a1a2e; color: #f48fb1; padding: 0.15em 0.4em; border-radius: 4px; }
|
||||
a { color: var(--color-highlight); }
|
||||
</style>
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Kapitel 5 — Testing
|
||||
## Unit · Integration · E2E · TDD
|
||||
|
||||
---
|
||||
|
||||
# Warum testen?
|
||||
|
||||
| Vorteil | Was bedeutet das |
|
||||
|---------|------------------|
|
||||
| **Confidence** | Refactor ohne Angst |
|
||||
| **Regression-Schutz** | alter Bug taucht nicht wieder auf |
|
||||
| **Living Documentation** | Tests zeigen, was Code soll |
|
||||
| **CI/CD-Voraussetzung** | automatisch vor jedem Deploy |
|
||||
|
||||
**„Untested Code is broken by design."**
|
||||
|
||||
---
|
||||
|
||||
# Test-Pyramide
|
||||
|
||||
```
|
||||
/\
|
||||
/E2E\ wenige (langsam, fragil)
|
||||
/-----\
|
||||
/ Integ \ einige (mittel)
|
||||
/---------\
|
||||
/ Unit \ viele (schnell, isoliert)
|
||||
/-------------\
|
||||
```
|
||||
|
||||
**Faustregel:** 70 % Unit, 20 % Integration, 10 % E2E.
|
||||
|
||||
---
|
||||
|
||||
# Vitest — modernes JS-Testing
|
||||
|
||||
```sh
|
||||
npm install -D vitest
|
||||
```
|
||||
|
||||
```javascript
|
||||
// utils.test.js
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { greet } from './utils.js';
|
||||
|
||||
describe('greet', () => {
|
||||
it('returns greeting with name', () => {
|
||||
expect(greet('Ada')).toBe('Hallo Ada');
|
||||
});
|
||||
|
||||
it('handles empty string', () => {
|
||||
expect(greet('')).toBe('Hallo ');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```sh
|
||||
npx vitest
|
||||
npx vitest --coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Test-Struktur (AAA)
|
||||
|
||||
```javascript
|
||||
it('adds two numbers', () => {
|
||||
// ARRANGE — Setup
|
||||
const a = 2;
|
||||
const b = 3;
|
||||
|
||||
// ACT — die Operation
|
||||
const result = add(a, b);
|
||||
|
||||
// ASSERT — Erwartung
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
```
|
||||
|
||||
**Eine Erwartung pro Test** (Faustregel).
|
||||
|
||||
---
|
||||
|
||||
# Vitest-Matchers
|
||||
|
||||
| Matcher | Was |
|
||||
|---------|-----|
|
||||
| `.toBe(x)` | strict equal (`===`) |
|
||||
| `.toEqual(x)` | deep equal (Objects, Arrays) |
|
||||
| `.toBeNull()` / `.toBeUndefined()` | |
|
||||
| `.toBeTruthy()` / `.toBeFalsy()` | |
|
||||
| `.toContain(item)` | Array enthält |
|
||||
| `.toHaveLength(n)` | |
|
||||
| `.toThrow()` | wirft Fehler |
|
||||
| `.toMatchSnapshot()` | Snapshot-Test |
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# TDD — Test-Driven Development
|
||||
|
||||
---
|
||||
|
||||
# TDD-Cycle: Red → Green → Refactor
|
||||
|
||||
1. **Red:** Test schreiben (schlägt fehl)
|
||||
2. **Green:** minimaler Code, damit Test besteht
|
||||
3. **Refactor:** Code säubern, Test bleibt grün
|
||||
|
||||
**Kent Beck, 2002.** Steile Lernkurve, aber stark fokussiert.
|
||||
|
||||
```javascript
|
||||
// 1. Red — Test ohne Implementierung
|
||||
it('isEven returns true for even', () => {
|
||||
expect(isEven(4)).toBe(true);
|
||||
});
|
||||
// → ReferenceError: isEven is not defined
|
||||
|
||||
// 2. Green — minimale Lösung
|
||||
function isEven(n) { return n % 2 === 0; }
|
||||
|
||||
// 3. Refactor — Edge Cases, Lesbarkeit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Integration-Tests
|
||||
|
||||
---
|
||||
|
||||
# API testen mit Supertest
|
||||
|
||||
```javascript
|
||||
import request from 'supertest';
|
||||
import { app } from './app.js';
|
||||
|
||||
describe('GET /users', () => {
|
||||
it('returns users array', async () => {
|
||||
const res = await request(app).get('/users');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toBeInstanceOf(Array);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /users', () => {
|
||||
it('creates a user', async () => {
|
||||
const res = await request(app)
|
||||
.post('/users')
|
||||
.send({ name: 'Ada' });
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.name).toBe('Ada');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Test-Datenbank
|
||||
|
||||
```javascript
|
||||
import { beforeAll, afterEach, afterAll } from 'vitest';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
let db;
|
||||
|
||||
beforeAll(() => {
|
||||
db = new Database(':memory:'); // in-RAM, super schnell
|
||||
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.exec('DELETE FROM users'); // sauberer Start
|
||||
});
|
||||
|
||||
afterAll(() => db.close());
|
||||
```
|
||||
|
||||
`:memory:`-DB = jeder Test isoliert, blitzschnell.
|
||||
|
||||
---
|
||||
|
||||
# Mocking
|
||||
|
||||
```javascript
|
||||
import { vi, it, expect } from 'vitest';
|
||||
|
||||
it('calls fetch with right URL', async () => {
|
||||
const mockFetch = vi.spyOn(global, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ name: 'Ada' })
|
||||
});
|
||||
|
||||
await getUserData(1);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api/users/1');
|
||||
});
|
||||
```
|
||||
|
||||
**Mock sparsam** — übermockt-Tests testen Mocks, nicht Code.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# E2E — End-to-End
|
||||
|
||||
---
|
||||
|
||||
# Playwright — moderne E2E
|
||||
|
||||
```sh
|
||||
npm init playwright@latest
|
||||
```
|
||||
|
||||
```javascript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('login flow', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
await page.getByLabel('Email').fill('ada@example.com');
|
||||
await page.getByLabel('Password').fill('secret123');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page.getByText('Hallo Ada')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
Browser tatsächlich öffnen + interagieren. **Realer User-Flow.**
|
||||
|
||||
---
|
||||
|
||||
# Playwright-Locators
|
||||
|
||||
```javascript
|
||||
// Bevorzugt: rollenbasiert (A11y-konform!)
|
||||
page.getByRole('button', { name: 'Submit' });
|
||||
page.getByLabel('Username');
|
||||
page.getByText('Welcome');
|
||||
|
||||
// Fallback: CSS-Selektor
|
||||
page.locator('.submit-btn');
|
||||
page.locator('#username');
|
||||
```
|
||||
|
||||
**Rollenbasierte Locators** finden Bugs, die A11y betreffen.
|
||||
|
||||
---
|
||||
|
||||
# E2E vs. Unit — Trade-offs
|
||||
|
||||
| | Unit | E2E |
|
||||
|--|------|-----|
|
||||
| Geschwindigkeit | ms | s |
|
||||
| Isolation | hoch | gering |
|
||||
| Realismus | gering | hoch |
|
||||
| Wartung | wenig | viel |
|
||||
| Wann | jede Funktion | kritische Flows |
|
||||
|
||||
E2E nicht für jede Detail-Logik — für **happy path + Login + Bezahlung**.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# Coverage + CI
|
||||
|
||||
---
|
||||
|
||||
# Coverage-Report
|
||||
|
||||
```sh
|
||||
npx vitest --coverage
|
||||
```
|
||||
|
||||
```
|
||||
File | % Stmts | % Branch | % Funcs | Uncovered
|
||||
-------------+---------+----------+---------+-----------
|
||||
utils.js | 100 | 100 | 100 |
|
||||
api.js | 75.3 | 60 | 80 | 42-48
|
||||
auth.js | 91.2 | 85 | 100 | 15
|
||||
```
|
||||
|
||||
**100 % ≠ keine Bugs.** Aber 0 % = sicher Bugs.
|
||||
|
||||
---
|
||||
|
||||
# Coverage-Ziel realistisch
|
||||
|
||||
| Code-Typ | Realistisch |
|
||||
|----------|-------------|
|
||||
| Pure Funktionen | 100 % |
|
||||
| Business-Logik | 80-90 % |
|
||||
| API-Routes | 70-80 % |
|
||||
| UI-Komponenten | 60-70 % |
|
||||
| Drittpartei-Wrapper | 0 % |
|
||||
| Konfig-Files | 0 % |
|
||||
|
||||
**Faustregel:** über 80 % Gesamt = gut. Unter 50 % = gefährlich.
|
||||
|
||||
---
|
||||
|
||||
# CI: Tests auf jedem Push
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run test:e2e
|
||||
```
|
||||
|
||||
Tests sind grün → PR mergebar. Sonst: rot, mergen verboten.
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: aufgabe -->
|
||||
|
||||
# Selbstlernen — Tests schreiben
|
||||
|
||||
Erweitert eure API:
|
||||
|
||||
1. **3+ Unit-Tests** für eure Helper-Funktionen (`utils.test.js`)
|
||||
2. **3+ Integration-Tests** für eure REST-Endpunkte (mit Supertest)
|
||||
3. **In-Memory-DB** in `beforeAll`/`afterEach`
|
||||
4. **1 E2E-Test** mit Playwright (z.B. Login-Flow)
|
||||
5. **Coverage-Report** generieren
|
||||
|
||||
**Bonus:** GitHub-Actions-Workflow für CI.
|
||||
|
||||
---
|
||||
|
||||
# Zusammenfassung
|
||||
|
||||
**Heute gelernt:**
|
||||
- **Test-Pyramide:** viele Unit, einige Integration, wenige E2E
|
||||
- **Vitest** als modernes Test-Runner-Setup
|
||||
- **AAA-Pattern** + Matchers + Lifecycle-Hooks
|
||||
- **TDD-Cycle** Red → Green → Refactor
|
||||
- **Supertest** für API-Integration-Tests
|
||||
- **Playwright** für E2E mit rollenbasierten Locators
|
||||
- **Coverage** als Werkzeug, nicht als Ziel
|
||||
- **CI** mit GitHub-Actions
|
||||
|
||||
**Nächste Stunde:** TypeScript — Typen über JavaScript.
|
||||
Reference in new issue
Block a user