Übungsaufgaben zu End-to-End-Tests mit Playwright: Mitfahrgesuche im Browser prüfen
https://bildung.social/@oerinformatik/
https://oer-informatik.de/vue-e2e-akzeptanztest-playwright-uebungsaufgaben
Playwright, End-to-End-Test, Abnahmetest, Kundentest, Locator, Auto-Waiting, page.route, Trace Viewer, instabile Tests, Vue 3, Übungsaufgaben, Prüfungsvorbereitung
tl/dr; (ca. 49 min Lese- und Bearbeitungszeit): Die Web-App des Mitfahrlabors soll Mitfahrgesuche annehmen, und ihre Kundentests sollen im echten Browser laufen. In diesen Übungen wählst Du Locator für die Seite der Gesuche und sagst voraus, welche Zeilen warten, schreibst End-to-End-Tests für das Einstellen eines Gesuchs mit nachgebildetem Backend, prüfst Fehlerfälle mit abgebrochenen und abgelehnten Anfragen und reparierst einen Test, der auf dem Teamrechner grün und in der Pipeline ab und zu rot ist [AI-Assisted]. (Zuletzt geändert am 11.11.2026)
Dieser Artikel ist Teil einer Artikelreihe über die Web-App mit Vue 3 und TypeScript: Projektübersicht / XP-Projektstart / TypeScript für Java-Entwickler*innen / Komponenten testgetrieben / API-Client aus OpenAPI / Router und Continuous Integration / Formulare und Validierung / Refactoring mit Pinia / Anmeldung mit Keycloak / Fahrer-Sicht und Coding Standards / Build und Auslieferung / End-to-End-Tests mit Playwright / Change Request Live-Aktualisierung
Diese Übungsaufgaben gehören zum Artikel End-to-End- und Akzeptanztests mit Playwright: Kundentests im echten Browser. Alle Aufgaben spielen im Projektteam der Mitfahr-App Mitfahrlabor. Ausgangspunkt für die Aufgaben 2.1, 2.2 und 3.1 ist das Projekt mitfahrlabor-web im Ordner src/11-vue-e2e-akzeptanztest-playwright des Repositorys dieser Reihe mit playwright.config.ts und den Hilfen meldeAnAls() (Datei e2e/hilfen/anmeldung.ts) und bildeBackendNach() (Datei e2e/hilfen/backend.ts). Was für die Gesuche fehlt, steht vollständig in den Aufgaben; die Lösungen der Übungen zu den vorigen Artikeln brauchst Du nicht.
Für die Gesuche nimmt das Team wie in den vorigen Übungen an, dass das Backend in einer späteren Version POST /v1/gesuche anbietet: Der Body enthält haltepunktId, richtung, datum, zeitVon und zeitBis, die Antwort ist bei Erfolg 201 mit dem gespeicherten Gesuch (mit id, mitfahrerId und status), bei einer verletzten Fachregel 422 mit Problem Details.
Hinweise zur Bearbeitung: Bis zur Teil-1-Prüfung der IT-Berufe solltest Du die Basiskompetenz beherrschen, also Aufgabe 1.1. Als FIA, FIDP oder FIDV musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1 und Aufgabe 2.2. Von FIA wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1) keine Probleme bereiten. Die Operatoren (nennen, angeben, beurteilen, vorhersagen, entwickeln, analysieren, erklären …) zeigen, wie ausführlich Du antworten sollst. Tests schreibst Du in TypeScript und prüfst sie mit npm run test:e2e, npm run type-check, npm run lint und npm run format:check; den Browser installierst Du einmalig mit npx playwright install chromium. Schreib Deine Lösung zuerst vollständig auf, bevor Du sie mit der Beispiellösung vergleichst.
Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?
Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.
Erstelle neue Übungsaufgaben zum Thema „End-to-End-Tests mit Playwright:
Mitfahrgesuche im Browser prüfen“.
Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.
Fachliche Grundlage:
- https://oer-informatik.de/vue-e2e-akzeptanztest-playwright
- https://oer-informatik.de/szenario-mitfahrlabor
- Stilbeispiel: https://oer-informatik.de/vue-e2e-akzeptanztest-playwright-uebungsaufgaben
Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Playwright
- End-to-End-Test
- Abnahmetest
- Kundentest
- Locator
- Auto-Waiting
- page.route
- Trace Viewer
- instabile Tests
- Vue 3
Kompetenzstufe: Kernkompetenz (alternativ: Basiskompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan
Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
danach frage.
Aufgabe 1.1: Locator für die Seite der Gesuche (Basiskompetenz)
Marie Schulz aus dem Projektteam hat eine Seite gebaut, die die eigenen offenen Gesuche zeigt. Im Browser entsteht daraus dieses HTML (gekürzt):
<h2>Offene Gesuche</h2>
<p role="status" class="anzahl">3 Gesuche</p>
<ul class="gesuche">
<li data-testid="gesuch-2"><strong>Herford</strong> zur Schule, 2026-11-19, 07:00–07:30
<button type="button" class="btn btn-sm btn-outline-danger">Zurückziehen</button></li>
<li data-testid="gesuch-4"><strong>Bielefeld</strong> nach Hause, 2026-11-20, 14:00–15:00
<button type="button" class="btn btn-sm btn-outline-danger">Zurückziehen</button></li>
<li data-testid="gesuch-5"><strong>Gütersloh</strong> zur Schule, 2026-11-24, 07:00–07:30
<button type="button" class="btn btn-sm btn-outline-danger">Zurückziehen</button></li>
</ul>
<form class="row">
<div class="col"><label for="gesuch-datum">Datum</label><input id="gesuch-datum" type="date" class="form-control"></div>
<div class="col"><button type="submit" class="btn btn-primary">Gesuch einstellen</button></div>
</form>a) Gib für jedes der vier Elemente einen Locator an, wie ihn das Team nach seiner Reihenfolge wählen würde: (1) die Überschrift, (2) die Meldung mit der Anzahl, (3) den Button „Zurückziehen“ des Gesuchs ab Bielefeld, (4) das Datumsfeld. (4 Punkte)
Je Locator 1 Punkt:
page.getByRole('heading', { name: 'Offene Gesuche' }) // (1)
page.getByRole('status') // (2)
page.getByRole('listitem').filter({ hasText: 'Bielefeld' })
.getByRole('button', { name: 'Zurückziehen' }) // (3)
page.getByLabel('Datum') // (4)Bei (3) ist auch page.getByTestId('gesuch-4').getByRole('button', { name: 'Zurückziehen' }) richtig; das Team nimmt data-testid aber erst, wenn nichts Sichtbares eindeutig ist, und der Ort ist hier sichtbar. Für (1) genügt auch getByRole('heading', { level: 2 }), solange es nur eine solche Überschrift gibt.
b) Jonas Wolff schlägt drei Locator vor. Beurteile jeden mit einem Satz: (1) page.locator('ul > li:nth-child(2) button') für den Button des Gesuchs ab Bielefeld, (2) page.getByRole('button', { name: 'Zurückziehen' }) mit anschließendem click(), (3) page.getByText('Gesuch') für die Meldung mit der Anzahl. (3 Punkte)
Je Beurteilung 1 Punkt:
- Findet heute den richtigen Button, hängt aber an Aufbau und Reihenfolge: Kommt ein Gesuch hinzu oder ändert sich die Sortierung, klickt der Test ein anderes Gesuch an, ohne dass die App einen Fehler hat.
- Findet drei Buttons; Locator sind strikt,
click()bricht mit „strict mode violation … resolved to 3 elements“ ab. Er muss auf einen Listeneintrag eingeschränkt werden. getByText()sucht ohneexactnach Teiltexten, ohne Groß- und Kleinschreibung zu beachten, und findet drei Elemente: „Offene Gesuche“, „3 Gesuche“ und den Button „Gesuch einstellen“. Für die Meldung passtgetByRole('status').
c) Sag für jede Zeile voraus, ob Playwright wartet, und wenn ja, worauf: (1) await page.getByRole('button', { name: 'Gesuch einstellen' }).click(), (2) expect(await page.getByRole('listitem').count()).toBe(3), (3) await expect(page.getByRole('status')).toHaveText('3 Gesuche'). (3 Punkte)
Je Zeile 1 Punkt:
- Wartet: Vor dem Klick prüft Playwright, ob der Button im DOM hängt, sichtbar und stabil ist, Ereignisse empfängt und aktiv ist (Actionability Checks).
- Wartet nicht auf die Bedingung:
count()zählt sofort einmal,expect()vergleicht nur noch einen Wert. Ist die Liste noch nicht geladen, ist das Ergebnis falsch. - Wartet: Web-First Assertion, Playwright sucht das Element wiederholt und vergleicht, bis der Text stimmt oder die Zeit (Standard 5 Sekunden) abgelaufen ist.
Aufgabe 2.1: Kundentests für „Gesuch einstellen“ (Kernkompetenz)
Oksana Melnyk (Pseudonym Kaffeetasse) soll ein Mitfahrgesuch über ein Formular einstellen können. Für die Karte hat Thomas Becker zwei Kundentests formuliert:
- KT-1: Kaffeetasse stellt ein Gesuch ab Herford zur Schule für den 24.11.2026 zwischen 07:00 und 07:30 Uhr ein. Danach steht dort „Gesuch 5 eingestellt“.
- KT-2: Für den 19.11.2026 zur Schule hat Kaffeetasse schon ein Gesuch. Ein zweites zeigt die Meldung des Backends „Für diesen Tag und diese Richtung hast Du schon ein Gesuch.“, und es erscheint keine Erfolgsmeldung.
Das Paar hat die Seite schon gebaut. Lege sie als src/views/GesuchNeuSeite.vue an und ergänze in src/router/index.ts hinter der Route angebot-neu die Zeile { path: '/gesuche/neu', name: 'gesuch-neu', component: () => import('@/views/GesuchNeuSeite.vue') },:
<script setup lang="ts">
import { ref } from 'vue'
import { haltepunkte } from '@/domain/beispieldaten'
import { useSitzungStore } from '@/stores/sitzung'
// Startcode Übung 2.1: Gesuch einstellen (POST /v1/gesuche, spätere Version der API)
const sitzung = useSitzungStore()
const haltepunktId = ref<number | ''>('')
const richtung = ref<'zur-schule' | 'nach-hause'>('zur-schule')
const datum = ref('')
const zeitVon = ref('')
const zeitBis = ref('')
const meldung = ref('')
const fehler = ref('')
async function einstellen() {
meldung.value = ''
fehler.value = ''
const antwort = await fetch(`${import.meta.env.VITE_API_BASIS ?? '/v1'}/gesuche`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${sitzung.accessToken}` },
body: JSON.stringify({
haltepunktId: haltepunktId.value,
richtung: richtung.value,
datum: datum.value,
zeitVon: zeitVon.value,
zeitBis: zeitBis.value,
}),
})
if (antwort.status === 201) {
const gesuch = (await antwort.json()) as { id: number }
meldung.value = `Gesuch ${gesuch.id} eingestellt`
} else {
const problem = (await antwort.json().catch(() => ({}))) as { detail?: string }
fehler.value = problem.detail ?? 'Das Gesuch konnte nicht eingestellt werden.'
}
}
</script>
<template>
<section>
<h2>Mitfahrgesuch einstellen</h2>
<form class="row g-2" @submit.prevent="einstellen">
<div class="col-sm-4">
<label for="gesuch-haltepunkt" class="form-label">Haltepunkt</label>
<select id="gesuch-haltepunkt" v-model="haltepunktId" class="form-select" required>
<option v-for="h in haltepunkte" :key="h.id" :value="h.id">{{ h.ort }}</option>
</select>
</div>
<div class="col-sm-4">
<label for="gesuch-richtung" class="form-label">Richtung</label>
<select id="gesuch-richtung" v-model="richtung" class="form-select">
<option value="zur-schule">zur Schule</option>
<option value="nach-hause">nach Hause</option>
</select>
</div>
<div class="col-sm-4">
<label for="gesuch-datum" class="form-label">Datum</label>
<input id="gesuch-datum" v-model="datum" type="date" class="form-control" required />
</div>
<div class="col-sm-3">
<label for="gesuch-von" class="form-label">Frühestens</label>
<input id="gesuch-von" v-model="zeitVon" type="time" class="form-control" required />
</div>
<div class="col-sm-3">
<label for="gesuch-bis" class="form-label">Spätestens</label>
<input id="gesuch-bis" v-model="zeitBis" type="time" class="form-control" required />
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Gesuch einstellen</button>
</div>
</form>
<p role="status" class="mt-3">{{ meldung }}</p>
<p v-if="fehler" role="alert" class="alert alert-danger">{{ fehler }}</p>
</section>
</template>a) Entwickle in e2e/hilfen/gesuche.ts eine Funktion bildeGesucheNach(page), die POST /v1/gesuche nachbildet: Für den 19.11.2026 zur Schule antwortet sie mit 422 und Problem Details (Typ …/gesuch-doppelt, Detail wie in KT-2), sonst mit 201 und dem Gesuch mit der ID 5, mitfahrerId 3 und Status offen. Andere Methoden soll sie nicht beantworten. (5 Punkte)
import type { Page } from '@playwright/test'
/** POST /v1/gesuche nachbilden: Kaffeetasse hat für den 19.11.2026 zur Schule schon ein Gesuch. */
export async function bildeGesucheNach(page: Page): Promise<void> {
await page.route('**/v1/gesuche', async (route) => {
const anfrage = route.request()
if (anfrage.method() !== 'POST') {
return route.fallback() // andere Methoden: an frühere Routen weitergeben
}
const neu = anfrage.postDataJSON()
if (neu.datum === '2026-11-19' && neu.richtung === 'zur-schule') {
return route.fulfill({
status: 422,
contentType: 'application/problem+json',
json: {
type: 'https://mitfahrlabor.example/probleme/gesuch-doppelt',
title: 'Fachregel verletzt',
status: 422,
detail: 'Für diesen Tag und diese Richtung hast Du schon ein Gesuch.',
},
})
}
return route.fulfill({ status: 201, json: { id: 5, mitfahrerId: 3, status: 'offen', ...neu } })
})
}Bewertung: Route mit passendem Muster (1 Punkt), andere Methoden mit route.fallback() oder route.continue() weitergeben (1 Punkt), Body mit postDataJSON() lesen und die Regel prüfen (1 Punkt), 422 mit Problem Details und passendem Content-Type (1 Punkt), 201 mit ID 5 und den gesendeten Feldern (1 Punkt). Der Typ der Problem Details folgt im Projekt der Konstante PROBLEM_BASIS; jede eindeutige Adresse ist richtig.
b) Entwickle den Test für KT-1 in e2e/ges-gesuch-einstellen.spec.ts. Er soll außer der Meldung auch prüfen, dass die App genau die eingegebenen Werte an das Backend schickt. (6 Punkte)
import { expect, test } from '@playwright/test'
import { KAFFEETASSE, meldeAnAls } from './hilfen/anmeldung'
import { bildeGesucheNach } from './hilfen/gesuche'
test.describe('Gesuch einstellen', () => {
test.beforeEach(async ({ page }) => {
await bildeGesucheNach(page)
await meldeAnAls(page, KAFFEETASSE)
await page.goto('/gesuche/neu')
})
test('KT-1: Kaffeetasse stellt ein Gesuch ab Herford ein', async ({ page }) => {
await page.getByLabel('Haltepunkt').selectOption({ label: 'Herford' })
await page.getByLabel('Richtung').selectOption({ label: 'zur Schule' })
await page.getByLabel('Datum').fill('2026-11-24')
await page.getByLabel('Frühestens').fill('07:00')
await page.getByLabel('Spätestens').fill('07:30')
const gesendet = page.waitForRequest((r) => r.url().endsWith('/v1/gesuche') && r.method() === 'POST')
await page.getByRole('button', { name: 'Gesuch einstellen' }).click()
await expect(page.getByRole('status')).toHaveText('Gesuch 5 eingestellt')
expect((await gesendet).postDataJSON()).toEqual({
haltepunktId: 2,
richtung: 'zur-schule',
datum: '2026-11-24',
zeitVon: '07:00',
zeitBis: '07:30',
})
})
})Bewertung: Nachbildung und Anmeldung vor goto() (1 Punkt), Felder über getByLabel() und selectOption()/fill() (2 Punkte), Button über getByRole() (1 Punkt), Meldung mit Web-First Assertion (1 Punkt), gesendeter Body geprüft (1 Punkt). waitForRequest() muss vor dem Klick angelegt werden, sonst kann die Anfrage schon vorbei sein. Ebenso richtig: den Body in bildeGesucheNach() in einer Variablen festhalten und nach der Meldung prüfen. Datumsfelder füllt fill() immer im Format JJJJ-MM-TT, unabhängig von der Anzeige im Browser.
c) Entwickle den Test für KT-2 im selben describe-Block. (3 Punkte)
test('KT-2: Ein zweites Gesuch für denselben Tag zeigt die Meldung des Backends', async ({ page }) => {
await page.getByLabel('Haltepunkt').selectOption({ label: 'Herford' })
await page.getByLabel('Datum').fill('2026-11-19')
await page.getByLabel('Frühestens').fill('07:00')
await page.getByLabel('Spätestens').fill('07:30')
await page.getByRole('button', { name: 'Gesuch einstellen' }).click()
await expect(page.getByRole('alert')).toHaveText('Für diesen Tag und diese Richtung hast Du schon ein Gesuch.')
await expect(page.getByRole('status')).toBeEmpty()
})Bewertung: Eingaben ohne Richtung, weil „zur Schule“ vorausgewählt ist (oder mit, beides richtig) (1 Punkt), Meldung über getByRole('alert') (1 Punkt), fehlende Erfolgsmeldung geprüft (1 Punkt). Die Prüfung auf die leere Live-Region steht nach der Meldung; vorher wäre sie sofort erfüllt und ohne Aussage.
d) Erkläre, was beide Tests über die Karte zeigen und was nicht, und nenne, was ein Test im Projekt backend dafür bräuchte. (2 Punkte)
Sie zeigen, dass die Seite die Eingaben richtig schickt und beide Antworten richtig anzeigt (1 Punkt). Sie zeigen nicht, dass das echte Backend den Endpunkt so anbietet, die Fachregel so prüft und das Token annimmt; die Nachbildung beruht auf einer Annahme über die spätere API. Ein Test im Projekt backend bräuchte ein Backend mit POST /v1/gesuche, ein Konto im Realm und Daten, die vor jedem Lauf gleich sind (z. B. frisch gestartetes Backend), weil der Test ein Gesuch anlegt (1 Punkt).
Aufgabe 2.2: Fehlerfälle im Browser prüfen (Kernkompetenz)
Aylin Kaya hat gemeldet, dass die Suche auf ihrem Telefon im Funkloch nur „Fahrten werden geladen …“ zeigte und danach eine Fehlermeldung. Das Team möchte die Fehlerfälle der Suche auch im Browser absichern. Die Tests stehen in e2e/fehlerfaelle.spec.ts und beginnen so:
import { expect, test } from '@playwright/test'
import { KAFFEETASSE, meldeAnAls } from './hilfen/anmeldung'
import { bildeBackendNach } from './hilfen/backend'
test.beforeEach(async ({ page }) => {
await bildeBackendNach(page)
await meldeAnAls(page, KAFFEETASSE)
})a) Entwickle einen Test für KT-03.2: Ist das Backend nicht erreichbar, steht dort „Die Fahrten können gerade nicht geladen werden.“ und es erscheint keine Fahrt. (4 Punkte)
test('KT-03.2: Backend nicht erreichbar', async ({ page }) => {
// später registriert = wird zuerst gefragt: überschreibt für diesen Test die Route aus bildeBackendNach()
await page.route('**/v1/angebote', (route) => route.abort('connectionrefused'))
await page.goto('/')
await expect(page.getByRole('alert')).toHaveText('Die Fahrten können gerade nicht geladen werden.')
await expect(page.getByRole('listitem')).toHaveCount(0)
})Bewertung: eigene Route vor goto() (1 Punkt), route.abort() statt einer Antwort mit Fehlercode (1 Punkt), Meldung über getByRole('alert') (1 Punkt), keine Fahrt (1 Punkt). Eine Antwort mit Status 503 wäre ein anderer Fall (Backend erreichbar, aber gestört); die App zeigt dafür dieselbe Meldung, KT-03.2 meint aber die fehlende Verbindung.
b) Lehnt das Backend ein Token ab (401), soll die App „Deine Anmeldung ist abgelaufen.“ mit dem Button „Neu anmelden“ zeigen, und oben steht kein Pseudonym mehr. Entwickle den Test. (4 Punkte)
test('Token abgelehnt: Hinweis und Button „Neu anmelden“', async ({ page }) => {
await page.route('**/v1/angebote', (route) =>
route.fulfill({ status: 401, headers: { 'WWW-Authenticate': 'Bearer error="invalid_token"' } }),
)
await page.goto('/')
await expect(page.getByRole('alert')).toContainText('Deine Anmeldung ist abgelaufen.')
await expect(page.getByRole('button', { name: 'Neu anmelden' })).toBeVisible()
await expect(page.getByText('Angemeldet als')).toHaveCount(0)
})Bewertung: 401 mit WWW-Authenticate wie beim Resource Server (1 Punkt), Hinweis über getByRole('alert') mit toContainText(), weil der Button zum Text der Meldung gehört (1 Punkt), Button über Rolle und Namen (1 Punkt), kein Pseudonym (1 Punkt). Die Anmeldung aus meldeAnAls() bleibt nötig, sonst fragt die App das Backend gar nicht erst.
c) Erkläre, warum die Route aus a) die Route aus bildeBackendNach() überschreibt, und wozu route.fallback() dient. (2 Punkte)
Passen mehrere Routen zu einer Anfrage, fragt Playwright sie in umgekehrter Reihenfolge ihrer Registrierung; die im Test registrierte Route kommt nach der aus beforeEach und wird zuerst gefragt (1 Punkt). route.fallback() gibt die Anfrage an die nächste passende, früher registrierte Route weiter; so kann eine Route nur bestimmte Fälle übernehmen und den Rest der allgemeinen Nachbildung überlassen (1 Punkt).
d) Starte den Test aus b) mit npx playwright test e2e/fehlerfaelle.spec.ts --trace on und öffne den Trace. Nenne drei Angaben im Trace, an denen Du erkennst, warum die Seite den Hinweis zeigt. (3 Punkte)
Je Angabe 1 Punkt, zum Beispiel:
- im Netzwerk-Tab die Anfrage
GET /v1/angebotemit Status 401 und dem HeaderWWW-Authenticatein der Antwort, - der Request-Header
Authorization: Bearer …: Die App hat ein Token geschickt, das Backend hat es abgelehnt, - im DOM-Schnappschuss nach dem Laden der Absatz mit
role="alert"und dem Button, während der Kopf kein „Angemeldet als“ mehr enthält, - in der Konsole ggf. die Meldung des Browsers zur Antwort 401,
- in der Zeitleiste die Reihenfolge:
goto, Anfrage, Antwort 401, Erscheinen des Hinweises.
Aufgabe 3.1: Ein Test, der mal grün und mal rot ist (Profilkompetenz)
Jonas Wolff hat für WEB-02 diesen Test geschrieben. Auf seinem Rechner ist er immer grün. In der Pipeline scheitert er etwa bei jedem fünften Lauf, und seit die Pipeline zwei Wiederholungen hat, zeigt der Bericht ihn meist als „flaky“:
import { expect, test } from '@playwright/test'
import { KAFFEETASSE, meldeAnAls } from './hilfen/anmeldung'
import { bildeBackendNach } from './hilfen/backend'
// ABSICHTLICH FEHLERHAFT (Übung 3.1): läuft auf dem Teamrechner meist grün, scheitert in der Pipeline ab und zu
test('KT-02 Filter nach Haltepunkt', async ({ page }) => {
await bildeBackendNach(page, { verzoegerungMs: Number(process.env.VERZOEGERUNG ?? 0) })
await meldeAnAls(page, KAFFEETASSE)
await page.goto('/')
await page.waitForTimeout(500) // warten, bis die Liste da ist
await page.locator('#such-haltepunkt').selectOption('1')
await page.locator('form button').click()
const anzahl = await page.locator('li').count()
expect(anzahl).toBe(2)
expect(await page.locator('.treffer').textContent()).toContain('2 Fahrten gefunden')
})Die Meldung aus der Pipeline lautet:
Error: expect(received).toBe(expected) // Object.is equality
Expected: 2
Received: 0
a) Analysiere die Meldung: Erkläre, in welchem Zustand die Seite war, als gezählt wurde, warum der Test auf dem Teamrechner grün ist und wie Du den Fehler mit der Option verzoegerungMs von bildeBackendNach() zuverlässig nachstellst. (4 Punkte)
Nach dem Klick auf „Suchen“ lädt die App neu: Die Liste verschwindet, es steht „Fahrten werden geladen …“ da, bis die Antwort kommt. count() hat genau in diesem Moment gezählt und 0 Einträge gefunden (1 Punkt). count() und textContent() lesen sofort einmal; expect() vergleicht danach nur noch Zahlen bzw. Texte und wartet nicht (1 Punkt). Auf dem Teamrechner antwortet das nachgebildete Backend so schnell, dass die neue Liste meist schon steht, wenn gezählt wird; in der ausgelasteten Pipeline dauert es manchmal länger (1 Punkt). Nachstellen: VERZOEGERUNG=300 npx playwright test … (PowerShell: $env:VERZOEGERUNG = "300"), dann scheitert der Test bei jedem Lauf mit derselben Meldung (1 Punkt).
b) Nenne drei weitere Schwächen des Tests, auch wenn sie nicht zu dieser Meldung geführt haben, und erkläre jeweils die Folge. (3 Punkte)
Je Schwäche mit Folge 1 Punkt:
waitForTimeout(500): Jeder Lauf wird eine halbe Sekunde langsamer, und braucht die Liste einmal länger, hilft das Warten trotzdem nicht.#such-haltepunkt,form buttonundli: hängen an IDs, Aufbau und Elementtyp statt an dem, was Nutzende sehen.form buttonbricht, sobald das Formular einen zweiten Button bekommt;lizählt jeden Listeneintrag der Seite, auch aus einer anderen Liste.selectOption('1'): wählt über die Haltepunkt-ID, die niemand sieht; ändert sich die Reihenfolge oder ID in den Daten, filtert der Test still nach einem anderen Ort.- Name ohne vollständige Nummer und ohne erwartetes Ergebnis: Im Bericht ist nicht erkennbar, welcher Kundentest gemeint ist.
c) Entwickle eine stabile Fassung des Tests. Weise nach, dass sie mit verzoegerungMs von 800 grün ist und der ursprüngliche Test rot. (5 Punkte)
// Lösung 3.1: kein festes Warten, Locator nach Rolle und Beschriftung, Assertions mit automatischem Wiederholen
test('KT-02: Filter nach Haltepunkt Gütersloh zeigt die Angebote 1 und 2', async ({ page }) => {
await bildeBackendNach(page, { verzoegerungMs: Number(process.env.VERZOEGERUNG ?? 0) })
await meldeAnAls(page, KAFFEETASSE)
await page.goto('/')
await expect(page.getByRole('status')).toHaveText('4 Fahrten gefunden')
await page.getByLabel('Haltepunkt').selectOption({ label: 'Gütersloh' })
await page.getByRole('button', { name: 'Suchen' }).click()
await expect(page.getByRole('status')).toHaveText('2 Fahrten gefunden')
await expect(page.getByRole('listitem')).toHaveCount(2)
})Nachweis mit VERZOEGERUNG=800 npx playwright test e2e/instabil.spec.ts e2e/stabil.spec.ts: Der ursprüngliche Test scheitert mit „Expected: 2, Received: 0“, die neue Fassung ist grün; ohne Verzögerung sind beide grün.
Bewertung: Warten auf den Ausgangszustand statt waitForTimeout (1 Punkt), Feld über getByLabel() und Auswahl über die sichtbare Beschriftung (1 Punkt), Button über getByRole() (1 Punkt), Web-First Assertions für Anzahl und Meldung (1 Punkt), Nachweis mit Verzögerung (1 Punkt). Die erste Assertion vor der Suche ist wichtig: Ohne sie könnte die Suche starten, während die erste Liste noch lädt, und das Ergebnis der ersten Anfrage käme zuletzt.
d) Jonas schlägt vor, statt den Test umzuschreiben die Wiederholungen in der Pipeline auf fünf zu erhöhen und waitForTimeout(2000) zu nutzen. Beurteile den Vorschlag. (3 Punkte)
Der Vorschlag macht die Pipeline öfter grün, behebt aber die Ursache nicht (1 Punkt). Mehr Wiederholungen verstecken instabile Tests: Ein echter Fehler, der nur manchmal auftritt (z. B. eine verspätete Antwort, die eine neuere überschreibt), würde ebenfalls weggewiederholt, und das Vertrauen in rote Tests sinkt (1 Punkt). Zwei Sekunden festes Warten verlängern jeden Lauf um zwei Sekunden je Stelle und reichen trotzdem nicht, wenn die Pipeline einmal langsamer ist. Besser: auf eine sichtbare Bedingung warten wie in c), Wiederholungen bei zwei lassen und jeden als „flaky“ gemeldeten Test wie einen roten behandeln (1 Punkt).
Links und weitere Informationen
End-to-End- und Akzeptanztests mit Playwright: der Artikel zu diesen Übungen
Playwright: Locators, Playwright: Mock APIs und Playwright: Best Practices
Hinweis zur Nachnutzung als Open Educational Resource (OER)
Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zu End-to-End-Tests mit Playwright: Mitfahrgesuche im Browser prüfen” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/vue-e2e-akzeptanztest-playwright-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/programmierung/vue. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]
