Wie Progressive Web Apps zuverlässig getestet werden
Progressive Web Apps bringen Service Worker, Offline Fallbacks und Install Prompts mit, die klassische E2E Tests nicht abdecken. Wer Cypress oder Playwright ohne eigene Strategie für Netzwerksimulation, Cache Invalidierung und Manifest Validierung einsetzt, übersieht genau die Fehler, die Kunden offline oder nach einem Deploy mit veralteten Preisen konfrontieren. Dieser Artikel zeigt, wie PWA Testing in der Praxis zuverlässig funktioniert.
Inhaltsverzeichnis
- 1. Warum PWA-Testing sich von klassischem E2E-Testing unterscheidet
- 2. Service-Worker-Lebenszyklus verstehen und gezielt testen
- 3. Offline-Bedingungen in Cypress simulieren
- 4. Offline-Bedingungen in Playwright simulieren
- 5. Install-Prompt und Manifest-Validierung testen
- 6. Cache-Strategien und Invalidierung nach dem Deploy testen
- 7. E-Commerce-spezifische Fallstricke: veraltete Preise und Cache Poisoning
- 8. Lighthouse-PWA-Audits in der CI automatisieren
- 9. PWA-Testing-Ansätze im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum PWA-Testing sich von klassischem E2E-Testing unterscheidet
Klassisches E2E-Testing geht von einem einfachen Modell aus: Der Browser sendet einen Request, der Server antwortet, das DOM aktualisiert sich entsprechend. Eine Progressive Web App schiebt eine zusätzliche Schicht dazwischen, den Service Worker, der als programmierbarer Proxy zwischen Browser und Netzwerk sitzt. Er kann Requests direkt aus dem Cache Storage beantworten, ganz ohne dass überhaupt ein Server-Request stattfindet. Genau das bricht die Annahme vieler klassischer Testsuiten, dass jede Netzwerkantwort direkt vom Backend unter Test stammt. Ein Test, der eine API mit cy.intercept() stubbt, sieht unter Umständen trotzdem eine gecachte Antwort des Service Workers, weil dieser dem Fetch-Event zuvorkommt, bevor der Stub überhaupt greifen kann.
Drei Testflächen kommen bei PWAs neu hinzu, die es in klassischen, rein serverseitig gerenderten Anwendungen nicht gibt: der Lebenszyklus des Service Workers mit seinen Install-, Activate- und Fetch-Phasen, das Offline-Verhalten der Anwendung ohne jede Netzwerkverbindung, und der Install-Flow über das Web App Manifest. Jede dieser Flächen kann unabhängig vom eigentlichen Anwendungscode kaputtgehen. Ein zweiter wichtiger Unterschied betrifft die Testisolation: Ein Service Worker, der in einem Testlauf registriert wurde, bleibt im Browserprofil aktiv und kann nachfolgende Tests mit veralteten Caches beeinflussen, wenn zwischen Testläufen nicht explizit caches.keys() geleert und die Service-Worker-Registrierung aufgehoben wird. Ohne diese Bereinigung entstehen die klassischen, schwer reproduzierbaren PWA-Testflakes.
2. Service-Worker-Lebenszyklus verstehen und gezielt testen
Der Service-Worker-Lebenszyklus durchläuft drei feste Phasen: install, activate und fetch. Im install-Event lädt der Service Worker die definierten Dateien in einen versionierten Cache. Im activate-Event räumt er alte Cache-Versionen auf und übernimmt mit self.clients.claim() sofort die Kontrolle über bereits offene Tabs, statt auf einen Reload zu warten. Erst danach greift der fetch-Handler, der über die eigentliche Caching-Strategie pro Request entscheidet. Ein Test, der diesen Ablauf ignoriert und sofort nach cy.visit() Assertions gegen gecachte Inhalte macht, testet häufig gegen einen Service Worker, der noch gar nicht aktiv ist.
Um den Lebenszyklus gezielt zu prüfen, wartet ein zuverlässiger Test explizit auf navigator.serviceWorker.ready beziehungsweise auf navigator.serviceWorker.controller, bevor er offline-relevante Assertions ausführt. Playwright bietet zusätzlich context.serviceWorkers(), das in Chromium-basierten Browsern eine Liste aller aktiven Service-Worker-Instanzen im Kontext zurückgibt und sich direkt inspizieren lässt. Für Deploy-Tests ist self.skipWaiting() im install-Event entscheidend: Ohne diesen Aufruf bleibt ein neuer Service Worker im waiting-Zustand hängen, bis alle Tabs der alten Version geschlossen wurden, was in E2E-Suiten zu Timeouts führt, wenn der Test genau dieses Verhalten nicht erwartet.
// service-worker.js - install, activate and fetch lifecycle
const CACHE_VERSION = 'shop-cache-v3';
const PRECACHE_ASSETS = [
'/offline.html',
'/css/app.css',
'/js/app.js',
'/images/logo.svg'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_VERSION).then((cache) => cache.addAll(PRECACHE_ASSETS))
);
// Activate the new service worker immediately after install
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => key !== CACHE_VERSION)
.map((key) => caches.delete(key))
)
).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline.html'))
);
return;
}
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});
3. Offline-Bedingungen in Cypress simulieren
Cypress bietet kein natives context.setOffline()-Äquivalent. Der eingebaute cy.intercept()-Befehl kann mit { forceNetworkError: true } einzelne Requests gezielt fehlschlagen lassen, das betrifft aber ausschließlich Requests, die Cypress im Browser abfangen kann, nicht das Verhalten des Service Workers selbst und nicht die tatsächliche Netzwerkverbindung des Betriebssystems. Ein Service Worker, der einen Fetch-Handler registriert hat, sieht einen per forceNetworkError simulierten Fehler oft gar nicht, weil er selbst als erste Instanz auf das fetch-Event reagiert.
Für echte Offline-Emulation in Chrome-basierten Browsern bleibt nur der Weg über das Chrome DevTools Protocol, das Cypress intern über Cypress.automation('remote:debugger:protocol', …) zugänglich macht. Der Befehl Network.emulateNetworkConditions mit offline: true kappt die Verbindung auf Browser-Ebene, nicht nur einzelne Requests. Diese Technik ist inoffiziell, funktioniert zuverlässig nur in Chrome und Electron, nicht in Firefox oder WebKit, und muss nach jedem Test explizit zurückgesetzt werden, da sonst nachfolgende Tests fälschlich offline laufen. Diese Einschränkung ist einer der Hauptgründe, warum viele Teams für PWA-lastige Suiten zusätzlich oder ausschließlich auf Playwright setzen.
// cypress/e2e/pwa/offline-fallback.cy.js
describe('PWA offline fallback', () => {
it('shows the offline page once the service worker is active', () => {
cy.visit('/');
// Wait until the service worker has taken control of this page
cy.window()
.its('navigator.serviceWorker.controller')
.should('exist');
// cy.intercept only fakes failures for requests it can see in the
// browser's fetch/XHR layer, it does not put the OS network offline.
// For true offline emulation in Chrome-family browsers, drop to CDP:
cy.window().then(() =>
Cypress.automation('remote:debugger:protocol', {
command: 'Network.emulateNetworkConditions',
params: { offline: true, latency: 0, downloadThroughput: 0, uploadThroughput: 0 }
})
);
cy.visit('/checkout', { failOnStatusCode: false });
cy.get('[data-testid="offline-banner"]')
.should('be.visible')
.and('contain.text', 'Offline');
// Restore normal network conditions so later tests are unaffected
cy.window().then(() =>
Cypress.automation('remote:debugger:protocol', {
command: 'Network.emulateNetworkConditions',
params: { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }
})
);
});
});
4. Offline-Bedingungen in Playwright simulieren
Playwright löst dasselbe Problem mit einer einzigen, browserübergreifenden API: context.setOffline(true) schaltet die gesamte BrowserContext in einen Offline-Zustand, in dem jeder Netzwerk-Request fehlschlägt, unabhängig davon, ob er vom Anwendungscode, vom Service Worker oder von einem Drittanbieter-Skript stammt. Die Methode funktioniert identisch in Chromium, Firefox und WebKit, weil Playwright sie über den jeweiligen browsereigenen Automatisierungsprotokoll-Layer implementiert, nicht über einen browserspezifischen Workaround. Das macht Offline-Tests in Playwright deutlich stabiler und portabler als die CDP-Lösung in Cypress.
Der entscheidende Testablauf für PWAs: Seite einmal online laden, damit der Service Worker installieren und precachen kann, auf navigator.serviceWorker.controller warten, dann erst context.setOffline(true) setzen und die Seite erneut aufrufen. So lässt sich präzise unterscheiden, ob eine bereits besuchte Route aus dem Cache bedient wird und eine nie besuchte Route korrekt auf die Offline-Fallback-Seite umleitet. Nach jedem Offline-Testfall setOffline(false) zurückzusetzen ist Pflicht, da der Zustand sonst über test.describe-Blöcke hinweg bestehen bleibt und nachfolgende Tests unerklärlich fehlschlagen lässt.
// tests/pwa/offline-fallback.spec.ts
import { test, expect } from '@playwright/test';
test('shows a cached product page and offline banner without network', async ({ page, context }) => {
// Visit once online so the service worker can install and precache assets
await page.goto('/products/test-sku-001');
await page.waitForFunction(() => navigator.serviceWorker.controller !== null);
// Flip the whole browser context offline, no CDP flags required
await context.setOffline(true);
await page.goto('/products/test-sku-001');
await expect(page.getByTestId('offline-banner')).toBeVisible();
await expect(page.getByTestId('product-price')).toHaveAttribute('data-stale', 'true');
// Navigating to an uncached route should fall back to offline.html
await page.goto('/products/never-visited-sku');
await expect(page.getByRole('heading', { name: 'You are offline' })).toBeVisible();
await context.setOffline(false);
});
5. Install-Prompt und Manifest-Validierung testen
Das beforeinstallprompt-Event, das Chrome auslöst, wenn eine Seite die Installierbarkeitskriterien erfüllt, lässt sich in automatisierten Browsern kaum zuverlässig triggern. Chrome prüft dafür unter anderem Engagement-Heuristiken wie eine Mindestverweildauer, die in einem frischen Testkontext ohne Interaktionshistorie praktisch nie erfüllt sind. Der native Prompt selbst ist zudem ein Browser-UI-Element außerhalb des DOM, das kein Test-Framework direkt ansteuern kann. Der pragmatische Ansatz: Nicht den nativen Browser-Dialog testen, sondern die eigene, im DOM gerenderte Install-Banner-Komponente, die auf das Event reagiert.
Dazu wird das Event im Test synthetisch ausgelöst, etwa mit window.dispatchEvent(new Event('beforeinstallprompt')), wobei prompt() und userChoice als Mock-Funktionen ergänzt werden, damit die eigene UI-Logik unabhängig vom echten Browser-Verhalten prüfbar bleibt. Für das Manifest selbst reicht kein Unit-Test: Lighthouse validiert im PWA-Audit automatisch Pflichtfelder wie name, icons in mindestens 192px und 512px, start_url und display. Ein fehlendes maskable-Icon oder ein falscher theme_color-Wert führt nicht zu einem harten Fehler, aber zu einer schlechteren Installierbarkeits-Bewertung und potenziell zu einem inkonsistenten Icon auf dem Homescreen des Nutzers.
{
"name": "Mironsoft Shop",
"short_name": "MS Shop",
"start_url": "/?utm_source=pwa",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#365314",
"orientation": "portrait-primary",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
]
}
6. Cache-Strategien und Invalidierung nach dem Deploy testen
Drei Cache-Strategien decken die meisten PWA-Anwendungsfälle ab: Cache-First für statische Assets wie Bilder, Fonts und CSS, die sich selten ändern und deren URL sich bei einer Änderung typischerweise über einen Hash im Dateinamen ebenfalls ändert. Network-First für Inhalte, die aktuell sein müssen, etwa Preis- und Bestandsdaten, mit einem Cache-Fallback ausschließlich für den Offline-Fall. Stale-While-Revalidate als Kompromiss: Der Nutzer bekommt sofort die gecachte Antwort, während im Hintergrund eine frische Version geladen und für den nächsten Aufruf gespeichert wird. Die Wahl der falschen Strategie pro Endpunkt ist die häufigste Ursache für PWA-Bugs, die im Test unsichtbar bleiben, weil sie nur nach echten Deploys auftreten.
Der kritische Testfall ist die Cache-Invalidierung nach einem Deploy: Ein neuer Service Worker mit erhöhter CACHE_VERSION muss alte Caches im activate-Event zuverlässig löschen, sonst bedient die Anwendung nach dem Deploy weiterhin eine Mischung aus altem und neuem Code. Ein guter Testaufbau deployt eine geänderte Version der Anwendung gegen dieselbe Browserinstanz, ruft registration.update() auf, wartet auf den statechange auf activated und prüft anschließend über caches.keys(), dass nur noch die neue Cache-Version existiert. Ohne diesen Test bleibt ein vergessenes skipWaiting() oder eine falsch berechnete CACHE_VERSION oft wochenlang unentdeckt.
7. E-Commerce-spezifische Fallstricke: veraltete Preise und Cache Poisoning
Der gefährlichste PWA-Fehler im E-Commerce-Kontext ist eine Cache-First-Strategie auf preis- oder bestandsrelevanten Endpunkten. Wird die Produktdetailseite oder der Preis-Endpunkt versehentlich cache-first bedient, sieht ein Nutzer im Offline-Modus oder bei einem instabilen Netzwerk einen Preis von vor Stunden oder Tagen, ohne dass die Oberfläche diesen als veraltet kennzeichnet. Im schlimmsten Fall schließt der Nutzer einen Checkout mit falschem Preis ab, was zu Storno, Kulanz oder Vertrauensverlust führt. Ein E2E-Test, der genau diesen Fall abdeckt, lädt eine Produktseite online, ändert den Preis serverseitig, geht offline und prüft, dass die Oberfläche entweder den alten Preis explizit als data-stale="true" markiert oder den Checkout-Button deaktiviert.
Cache Poisoning ist die zweite Gefahr: Wird eine fehlerhafte oder personalisierte Antwort, etwa eine Serverfehlerseite oder ein Warenkorb mit fremden Artikeln, versehentlich unter einem generischen Cache-Key gespeichert, bekommen alle nachfolgenden Nutzer dieselbe falsche Antwort ausgeliefert, bis der Cache abläuft oder invalidiert wird. Der Fetch-Handler sollte deshalb nur Responses mit Status 200 cachen und niemals Requests mit Authorization-Header oder personalisierte Endpunkte wie /customer/account in einen gemeinsam genutzten Cache schreiben. Ein Regressionstest, der gezielt einen 500er-Response provoziert und anschließend prüft, dass dieser nicht im Cache landet, deckt diese Klasse von Fehlern zuverlässig auf, bevor sie in Produktion sichtbar wird.
8. Lighthouse-PWA-Audits in der CI automatisieren
Manuelle Lighthouse-Checks im Chrome DevTools-Panel decken PWA-Regressionen erst auf, wenn ein Entwickler zufällig danach schaut. Lighthouse CI (@lhci/cli) automatisiert genau diesen Audit bei jedem Pull Request und kann den Build fehlschlagen lassen, wenn definierte Schwellenwerte unterschritten werden. Die PWA-Kategorie in Lighthouse prüft unter anderem, ob ein Service Worker registriert ist, ob das Manifest gültig ist, ob eine Offline-Seite mit Statuscode 200 antwortet und ob die Seite über HTTPS ausgeliefert wird. Diese Checks laufen headless gegen einen echten Chrome-Prozess und liefern damit belastbarere Ergebnisse als reine Unit-Tests gegen die Service-Worker-Datei.
In der Praxis läuft lhci autorun gegen eine gebaute und lokal servierte Version der Anwendung, sammelt mehrere Durchläufe zur Reduktion von Messrauschen und vergleicht die Ergebnisse gegen eine lighthouserc.json mit Assertions wie categories:pwa mindestens 0.9. Schlägt der Schwellenwert fehl, bricht der CI-Job mit einer klaren Fehlermeldung ab, statt dass die Regression erst im nächsten manuellen Review auffällt. Diese Automatisierung ergänzt die funktionalen Cypress- und Playwright-Tests aus den vorigen Abschnitten, statt sie zu ersetzen: Lighthouse prüft die PWA-Konformität strukturell, die E2E-Tests prüfen das tatsächliche Nutzerverhalten bei Offline- und Install-Szenarien.
# .github/workflows/lighthouse-ci.yml
name: Lighthouse CI
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- run: npm install -g @lhci/cli@0.13.x
- run: lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
9. PWA-Testing-Ansätze im direkten Vergleich
Die Wahl des richtigen Werkzeugs für welche PWA-Testaufgabe hat direkten Einfluss auf Stabilität und Wartungsaufwand der Suite. Cypress und Playwright unterscheiden sich gerade bei Offline-Simulation, Service-Worker-Introspektion und Multi-Browser-Unterstützung deutlich, während sich viele funktionale Tests in beiden Frameworks nahezu identisch schreiben lassen.
| Testaufgabe | Cypress | Playwright | Empfehlung |
|---|---|---|---|
| Echte Offline-Emulation | Nur via inoffizielle CDP-Workarounds | context.setOffline() nativ | Playwright für offline-lastige Suiten |
| Service-Worker-Introspektion | Kein direkter API-Zugriff | context.serviceWorkers() | Playwright |
| Multi-Browser-Support | Nur Chrome-Familie stabil | Chromium, Firefox, WebKit | Playwright für Cross-Browser-Absicherung |
| PWA-Score / Manifest-Audit | Über externes Lighthouse-CLI | Über externes Lighthouse-CLI | Lighthouse CI unabhängig vom E2E-Tool |
| Setup für Standard-E2E-Tests | Sehr niedrig, große Community | Niedrig, native TypeScript-Unterstützung | Beide gleichwertig außerhalb PWA-Spezifika |
In der Praxis bewährt sich eine Kombination: Playwright für die Offline- und Service-Worker-lastigen Testfälle, in denen echte Multi-Browser-Absicherung und stabile context.setOffline()-Emulation zählen, und Lighthouse CI als strukturelle Ergänzung für Manifest- und PWA-Score-Regressionen. Teams, die bereits vollständig auf Cypress setzen, müssen PWA-Tests nicht zwingend migrieren, sollten aber die CDP-basierte Offline-Emulation als das erkennen, was sie ist: ein funktionierender, aber Chrome-spezifischer Workaround, der in gemischten Browser-Setups an Grenzen stößt.
Mironsoft
PWA-Testing, E2E-Automatisierung und CI/CD für Magento- und Hyvä-Shops
PWA-Tests, die Offline-Bugs vor dem Deploy fangen?
Wir bauen Cypress- und Playwright-Suiten für Service-Worker-Lebenszyklus, Offline-Fallbacks und Cache-Invalidierung auf und integrieren Lighthouse-PWA-Audits direkt in eure CI/CD-Pipeline.
Offline-Testsuiten
Cypress- und Playwright-Tests für Service Worker, Cache-Strategien und Offline-Fallback-Seiten
Manifest & Install-Flow
Validierung von Web App Manifest, Icons und der eigenen Install-Banner-Logik
Lighthouse CI
Automatisierte PWA-Score-Audits als Quality Gate in Pull Requests
10. Zusammenfassung
PWA-Testing unterscheidet sich von klassischem E2E-Testing durch drei zusätzliche Flächen: den Service-Worker-Lebenszyklus, echtes Offline-Verhalten und den Install-Flow über das Web App Manifest. Cypress deckt Standard-E2E-Szenarien zuverlässig ab, stößt bei echter Offline-Emulation aber auf inoffizielle CDP-Workarounds, die nur in Chrome-basierten Browsern funktionieren. Playwright löst dieselbe Aufgabe mit der nativen context.setOffline()-API browserübergreifend und bietet mit context.serviceWorkers() direkten Einblick in aktive Service-Worker-Instanzen.
Für den E-Commerce-Kontext ist die wichtigste Regel: Preis- und Bestandsdaten gehören niemals in eine Cache-First-Strategie, weil sonst veraltete Werte offline oder bei instabilem Netzwerk als aktuell erscheinen. Cache-Invalidierung nach jedem Deploy muss explizit getestet werden, ebenso wie die Absicherung gegen Cache Poisoning bei Fehlerantworten. Lighthouse CI ergänzt die funktionalen E2E-Tests um eine strukturelle, automatisierte Prüfung von Manifest, HTTPS und PWA-Score bei jedem Pull Request.
PWA-Testing-Strategie - Das Wichtigste auf einen Blick
Service-Worker-Lifecycle
install, activate und fetch gezielt testen, auf navigator.serviceWorker.ready warten statt sofort zu asserten.
Offline-Simulation
context.setOffline() in Playwright nativ und browserübergreifend, in Cypress nur via CDP-Workaround für Chrome.
Cache-Strategien
Cache-First nur für statische Assets, Network-First für Preis und Bestand, Stale-While-Revalidate als Kompromiss.
CI-Absicherung
Lighthouse CI als Quality Gate für Manifest, HTTPS und PWA-Score bei jedem Pull Request.