Kurzfassung

Das Page Object Model (POM) ist der Branchenstandard, lässt sich jedoch nicht einfach skalieren. Das Flow Model Pattern verbessert das grundlegende POM und bietet eine perfekt ausgewogene Lösung, die weder übermäßig komplex noch eingeschränkt skalierbar ist. Das Screenplay Pattern eignet sich am besten für hochkomplexe Lösungen und erfordert ein höheres technisches Know-how der Testingenieure. Stellt daher vor der Implementierung sicher, dass euer Team ein abstrakteres Framework warten kann. Falls das nicht möglich ist, entweder Programmier-Workshops abhalten oder beim Flow Model Pattern bleiben.

Vorwort

Wer sollte diesen Artikel lesen?

Testautomatisierungsingenieure, technische Architekten und Softwareentwickler, die nach optimalen End-to-End-Testautomatisierungslösungen suchen.

Alle, die an der Entwicklung einer End-to-End-Testautomatisierungsstrategie beteiligt oder daran interessiert sind (z. B. Testmanager, Test Automation Engineers, technische Architekten und Softwareentwickler).

Worum geht es in diesem Artikel?

Nach jahrelanger Arbeit mit verschiedenen Testautomatisierungs-Frameworks habe ich gelernt, dass die Wahl des richtigen Designmusters nicht darin besteht, das beste zu finden – sondern das richtige für das jeweilige Projekt und Team.

Die drei primären Muster sind:

  • Page Object Model
  • Flow Model Pattern (oder Facade Design Pattern)
  • Screenplay Pattern

Dieser Artikel untersucht, wann jedes Muster am besten geeignet ist, und stützt sich dabei auf meine praktische Erfahrung beim Aufbau und der Pflege von Automatisierungs-Frameworks in verschiedenen Größenordnungen. Ich beginne mit einem Überblick über jeden Ansatz und führe euch dann zu dem Muster, das am besten zu euren Anforderungen passt.

API-basiertes Modell: Eine universelle Best Practice

Bevor wir verschiedene Designmuster erkunden, möchte ich ein Prinzip hervorheben, das für alle gilt: den API-basierten Ansatz zur Testautomatisierung.

Mein Kollege Jovan Ilić befürwortet diesen Ansatz in seinem Artikel „Test Automation: API-based Model”, und ich empfehle ihn nachdrücklich – unabhängig davon, welches Muster ihr wählt.

Das Kernprinzip

Jede Aktion in der UI einmal durchführen – danach stets die API verwenden

Überprüft, ob die Oberfläche funktioniert, aber nutzt schnellere und zuverlässigere API-Aufrufe für Test-Setup, Navigation und Zustandsverwaltung in nachfolgenden Tests.

Warum es wichtig ist

Dieser Ansatz bietet mit jedem Muster erhebliche Vorteile:

  • Schnellere Testausführung
  • Weniger flakige Tests
  • Bessere Testisolierung
  • Einfachere Wartung
  • Fokussiertere UI-Abdeckung

Unabhängig davon, ob ihr das Page Object Model, das Flow Model Pattern oder das Screenplay Pattern verwendet, überlegt, welche Interaktionen wirklich eine UI-Validierung erfordern und welche effizienter über APIs abgewickelt werden können. Dieser pragmatische Ansatz verbessert eure Test-Suite unabhängig vom gewählten Architekturmuster.

Page Object Model

Überblick

Das Page Object Model (POM) ist das grundlegende Designmuster, das die meisten Test Automation Engineers zuerst erlernen. Die Kernidee ist einfach: Trennung der Seiteninteraktionslogik von Testskripten, indem UI-Elemente und ihre Aktionen in dedizierte Seitenklassen und -dateien gekapselt werden.

Flowchart of an e-commerce testing process with handwritten notes, arrows connecting steps, and sketched screens showing product selection and details for adding a product to the cart, illustrating the use of the Screenplay Pattern in test automation design.

Beschreibung

Das Page Object Model bleibt mein bevorzugter Ausgangspunkt für die meisten Projekte – aufgrund seiner bewährten Wirksamkeit und universellen Bekanntheit. Diese weite Verbreitung verkürzt die Einarbeitungszeit für Neulinge erheblich, da sie sich schnell an eine neue Code-Basis gewöhnen können, ohne einen völlig neuen Architekturansatz erlernen zu müssen.

Obwohl POM seine Grenzen hat – besonders wenn Anwendungen an Komplexität zunehmen –, machen seine Einfachheit und Direktheit es zu einem hervorragenden Fundament. Man kann es als Ausgangsbasis betrachten: unkompliziert zu implementieren, leicht zu verstehen und flexibel genug, um sich mit den Anforderungen des Projekts weiterzuentwickeln.

Code-Beispiel

// For the sake of simplicity and framework-agnosticity
// we're skipping the selectors/locators definitions and functions
// that are mentioned on the picture, but not used within test case
// ===========
// Test Script
// ===========
it('should add yellow t-shirt to cart', async () => {
    await ProductsPage.openProductDetails('tshirt-yellow-medium');
    await ProductDetailsPage.clickAddButton();
    const cartCount = await ProductDetailsPage.getCartCount();
    expect(cartCount).toBe(1);
});
// =============
// Products Page
// =============
export const ProductsPage = {
    async openProductDetails(id) { ... }
}
// ====================
// Product Details Page
// ====================
export const ProductDetailsPage = {
    async clickAddButton() { ... },
    async getCartCount() { ... }
}

Zusammenfassung

VORTEILE

  • Weit verbreiteter Standard – häufigstes Muster in der Testautomatisierung
  • Sehr intuitiv – leicht verständlich für Neulinge
  • Starke Community-Unterstützung – reichhaltige Tutorials, Beispiele und Hilfe verfügbar
  • Flexibel in allen Testphasen – DEV, SIT, UAT usw.
  • Reduziert Code-Duplikation – wiederverwendbare Seitenklassen über mehrere Tests hinweg
  • Gute Trennung der Zuständigkeiten – Testlogik getrennt von Locators und Interaktionslogik

NACHTEILE

  • In reiner Form schwer wartbar – repetitiver Code, Seitenklassen können aufgebläht werden (Hunderte/Tausende von Zeilen)
  • Verletzt SOLID-Prinzipien – insbesondere das Single-Responsibility-Prinzip
  • Weniger effizient für Anwendungen, bei denen API-Tests geeigneter wären als UI-Tests
  • UI-zentrisches Denken – fördert das Testen von allem über die UI
  • Mangelnde Standardisierung – Implementierungen variieren stark zwischen Teams
  • Schlechte Skalierbarkeit – wird mit wachsender Anwendung zunehmend schwieriger zu verwalten

Flow Model Pattern

Überblick

Das Flow Model Pattern erweitert das Page Object Model durch eine Workflow-Abstraktionsschicht zwischen Tests und Page Objects. Diese Schicht erfasst wiederverwendbare Geschäfts-Workflows, reduziert Code-Duplikation und hält sowohl Page Objects als auch Testskripte sauber und fokussiert.

Hand-drawn diagram showing steps for adding 20 products in a test script, featuring the shopping flow, product/pages, and relationships between test scripts, flows, page objects, and the application under test. Integrates clear references to the Page Object Model and Test Automation Design Pattern principles. Includes simple shirt icons.

Beschreibung

Bei der Entwicklung meines neuesten Testautomatisierungs-Frameworks befand ich mich zwischen zwei Extremen: POMs fühlten sich zu simplistisch an und führten zu Duplikation, während das Screenplay Pattern für ein Projekt unter Termindruck, in dem dem Team die Erfahrung mit fortgeschrittenen Mustern fehlte, zu komplex erschien.

Ich brauchte etwas, das das Beste aus beiden Welten vereint – so einfach und unkompliziert wie das Page Object Model, aber mit besserer Wiederverwendbarkeit. Die Lösung bestand darin, eine einzige Abstraktionsschicht hinzuzufügen, die POMs auf das konzentriert, was sie am besten können (Anwendungsseiten abbilden), ohne die Komplexität einer vollständigen Screenplay-Implementierung.

Ich nannte diesen Ansatz „User Steps”, nur um später beim Lesen des ISTQB Test Automation Engineering Syllabus zu entdecken, dass er bereits einen Namen hatte: Flow Model Pattern. Es stellte sich heraus, dass ich nicht so originell war, wie ich dachte!

Das Letzte, was ich über das Flow Model Pattern erwähnen möchte, ist, dass es nicht (sehr gut) standardisiert ist, sodass man völlige Flexibilität hat, es an die eigenen Bedürfnisse anzupassen. Das macht es besonders gut geeignet für kleine bis mittelgroße Teams, die eine bessere Struktur als reines POM wünschen, ohne die Lernkurve des Screenplay Patterns.

Code-Beispiel

// Der Einfachheit halber und um Framework-Unabhängigkeit zu gewährleisten,
// lassen wir die Definitionen von Selektoren/Locators und Funktionen weg,
// die auf dem Bild erwähnt, aber im Testfall nicht verwendet werden

// ===========
// TEST SCRIPT
// ===========

test(‘should add 20 t-shirts to cart’, async () => {
await ShoppingFlow.addDiverseTShirts(20);

const cartCount = await ShoppingFlow.getCartItemCount();
expect(cartCount).toBe(20);
});

// =============
// SHOPPING FLOW
// =============

export const ShoppingFlow = {
async addProductToCart(productId) {
await ProductsPage.openProductDetails(productId);
await ProductDetailsPage.clickAddButton();
await ProductDetailsPage.goBackToProductsPage();
},
async addMultipleProducts(productIds) {
for (const id of productIds) {
await this.addProductToCart(id);
}
},
async getCartItemCount() {
return await ProductDetailsPage.getCartCount();
},
async addDiverseTShirts(count) {
const tshirts = TShirtFactory.createDiverseSet(count);
const productIds = tshirts.map(shirt => shirt.id);
await this.addMultipleProducts(productIds);
}
}

// ================================================================
// TEST DATA FACTORY – Clean way to handle the Test Data generation
// ================================================================

export const TShirtFactory = {
create(color, size) {
const id = `tshirt-${color.toLowerCase()}-${size.toLowerCase()}`;
return {
id: id,
name: `T-Shirt Model ${color} ${size}`,
color: color,
size: size
};
},
createDiverseSet(count) {
const colors = [‘yellow’, ‘black’, ‘blue’, ‘white’, ‘green’, ‘turquoise’];
const sizes = [‘small’, ‘medium’, ‘large’];

return Array(count).fill(null).map((_, index) => {
const color = colors[index % colors.length];
const size = sizes[index % sizes.length];
return this.create(color, size);
});
}
};

// =============
// PRODUCTS PAGE
// =============

export const ProductsPage = {
async openProductDetails(id) { … }
}

// ====================
// PRODUCT DETAILS PAGE
// ====================

export const ProductDetailsPage = {
async clickAddButton() { … },
async getCartCount() { … },
async goBackToProductsPage() { … }
}

Zusammenfassung

VORTEILE

  • Pragmatischer Mittelweg – ausgewogenes Verhältnis zwischen Einfachheit und Skalierbarkeit
  • Reduziert Code-Duplikation – Workflows an einem zentralen Ort
  • Hält Page Objects sauber – POMs konzentrieren sich nur auf Seiteninteraktionen
  • Leicht erlernbar – intuitives Konzept, schnelles Onboarding
  • Verbessert die Lesbarkeit von Tests – Tests fokussieren auf WAS, nicht auf WIE
  • Schnell zu implementieren – kann schrittweise zu bestehenden Frameworks hinzugefügt werden
  • Keine Framework-Abhängigkeit – funktioniert mit jedem Tool
  • Gut skalierbar für mittlere Projekte – bewältigt Wachstum ohne überwältigende Komplexität

NACHTEILE

  • Nicht standardisiert – keine offizielle Spezifikation oder Branchenkonsens
  • Grenzentscheidungen – erfordert Urteilsvermögen darüber, was in Flows vs. Pages gehört
  • Begrenzte Dokumentation – weit weniger Ressourcen als POM oder Screenplay
  • Kann SOLID verletzen – Risiko, zu einer Logik-„Abladestelle” zu werden
  • Weniger Struktur – Team muss eigene Konventionen festlegen
  • Risiko der Überabstraktion – kann unnötige Komplexität erzeugen, wenn zu exzessiv eingesetzt

Screenplay Pattern

Überblick

Das Screenplay Pattern wechselt von seitenzentriertem (oder UI-zentriertem) zu akteurzentriertem Testen. Tests beschreiben, WER welche Aktionen ausführt, um seine Ziele zu erreichen, mithilfe von Abilities, Tasks, Interactions und Questions. Dieser akteurbasierte Ansatz glänzt in komplexen Multi-Interface-Szenarien.

A hand-drawn workflow for testing a shirts web app, illustrating adding products, key actions, and a test script model that connects actors, tasks, abilities, interactions, questions, and the application under test—demonstrating how the Screenplay Pattern streamlines your Test Automation Design Pattern for clear and maintainable tests.

Beschreibung

Im Laufe meiner Karriere, in der ich mit allen wichtigen Testautomatisierungs-Frameworks gearbeitet habe, habe ich ein interessantes Muster beobachtet: Obwohl das Screenplay Pattern in der QA-Community bekannt ist, bleibt praktische Implementierungserfahrung vergleichsweise selten. Ich selbst habe es einmal in einer Produktionsumgebung implementiert, und diese Erfahrung – kombiniert mit umfangreicher Recherche – hat mir wertvolle Einblicke gegeben, warum die Verbreitung begrenzt bleibt.

Mit dieser Implementierungslücke stehe ich nicht allein da. Die Testautomatisierungs-Community ringt häufig damit, wann und wie verschiedene Muster angewendet werden sollen, wie laufende Diskussionen wie der Reddit-Thread „I don’t get the ScreenPlay Pattern” belegen. Was fehlt, ist nicht nur technische Dokumentation – es ist praktische Orientierung zur Musterwahl auf Basis realer Einschränkungen und Team-Fähigkeiten.

Diese Wissenslücke hat erhebliche Auswirkungen. Viele Teams greifen standardmäßig auf das Page Object Model zurück und erweitern es nach eigenem Ermessen – nicht weil es die optimale Lösung ist, sondern weil es vertraut ist. Unterdessen verwenden Projekte, die von ausgefeilteren Ansätzen profitieren könnten, weiterhin Muster, die letztlich ihre Skalierbarkeit und Wartbarkeit behindern. Mein Ziel mit diesem Artikel ist es, diese Lücke zu schließen, indem ich den kontextuellen Entscheidungsrahmen biete, der in den meisten Automatisierungsdiskussionen derzeit fehlt – zusammen mit klaren visuellen Erklärungen, die euch das Screenplay Pattern vielleicht endlich „verstehen” lassen.

Warum das Screenplay Pattern anders ist

Das Screenplay Pattern ist nicht einfach „POM mit zusätzlichen Schichten” – es ist eine grundlegend andere Art, über Testautomatisierung nachzudenken. Anstatt Code um Seiten und Web-Elemente zu organisieren, organisiert man ihn um Akteure (Benutzer oder Systeme) und ihre Absichten.

Was es einzigartig macht:

  • Akteurzentriertes Denken: Tests lesen sich wie User Stories („James meldet sich an und legt ein Produkt in den Warenkorb”)
  • Trennung der Zuständigkeiten: Abilities, Tasks, Interactions und Questions haben jeweils eine einzige, klare Verantwortung
  • Kombinierbarkeit: Kleine, wiederverwendbare Teile lassen sich zu komplexen Workflows zusammensetzen
  • Multi-Interface-Unterstützung: Derselbe Akteur kann nahtlos mit UI, API und Datenbank interagieren
  • Skalierbarkeit: Architektur, die Enterprise-Komplexität bewältigt, ohne unhandlich zu werden

Der Kompromiss

Der Preis für diese Raffinesse ist Komplexität. Das Screenplay Pattern erfordert:

  • Fundiertes Verständnis objektorientierter Designprinzipien (insbesondere SOLID)
  • Längere anfängliche Einrichtung und Lernkurve
  • Team-Commitment und Schulungsinvestitionen
  • Bekenntnis zur Struktur des Musters

Für große, langfristige Projekte mit erfahrenen Teams zahlen sich diese Investitionen aus. Für kleinere Projekte oder Teams ohne fundierte Programmierkenntnisse kann das Muster wie Overengineering wirken. Um völlig ehrlich zu sein: Die Implementierung in solchen Fällen macht selten Sinn – der Return on Investment rechtfertigt den Aufwand schlicht nicht.

Code-Beispiel

// Der Einfachheit halber und um Framework-Unabhängigkeit zu gewährleisten,
// lassen wir die Definitionen von Selektoren/Locators und Funktionen weg,
// die auf dem Bild erwähnt, aber im Testfall nicht verwendet werden

// ===========
// TEST SCRIPT
// ===========

// src/tests/Add20Tshirts.js
test(‘should add 20 t-shirts to cart’, async () => {
const sarah = Actor.named(‘Sarah’);

await sarah.attemptsTo(
AddDiverseTShirtsToCart.count(20)
);

const cartCount = await sarah.asks(CartItemCount.value());
expect(cartCount).toBe(20);
});

// ==========================================================================
// AKTEUR – PERSONEN und EXTERNE SYSTEME, die mit dem zu testenden System interagieren
// ==========================================================================

// src/actor.js
export const Actor = {
named(name) {
return {
name: name,
ability: BrowseTheWeb,

async attemptsTo(…tasks) {
for (const task of tasks) {
await task.performAs(this);
}
},

async asks(question) {
return await question.answeredBy(this);
}
};
}
};

// =====================================================================================
// ABILITIES – WRAPPERS around any INTEGRATION LIBRARIES (e.g. E2E, API, Cloud Services)
// =====================================================================================

// src/abilities.js
export const BrowseTheWeb = {
async findElement(selector) { /* … */ },
async click(selector) { /* … */ },
async getText(selector) { /* … */ }
};

// ==========================================================================
// TASKS – AKTIVITÄTSSEQUENZEN als bedeutsame Schritte eines Geschäfts-Workflows
// ==========================================================================

// src/tasks/AddDiverseTShirtsToCart.js
export const AddDiverseTShirtsToCart = {
count(numberOfShirts) {
return {
numberOfShirts: numberOfShirts,

async performAs(actor) {
const tshirts = TShirtFactory.createDiverseSet(this.numberOfShirts);

for (const tshirt of tshirts) {
await actor.attemptsTo(
AddProductToCart.withId(tshirt.id)
);
}
}
};
}
};

// src/tasks/AddProductToCart.js
export const AddProductToCart = {
withId(productId) {
return {
productId: productId,

async performAs(actor) {
await actor.attemptsTo(
OpenProductDetails.for(this.productId),
Click.on(‘.add-button’),
Click.on(‘.back-to-products’)
);
}
};
}
};

// ================================================================================
// INTERACTIONS – NIEDRIGSTUFIGE AKTIVITÄTEN, die ein AKTEUR über eine Schnittstelle ausführen kann
// ================================================================================

// src/interactions/OpenProductDetails.js
export const OpenProductDetails = {
for(productId) {
return {
productId: productId,

async performAs(actor) {
const selector = `[data-product-id=”${this.productId}”]`;
await actor.attemptsTo(
Click.on(selector)
);
}
};
}
};

// src/interactions/Click.js
export const Click = {
on(selector) {
return {
selector: selector,

async performAs(actor) {
await actor.ability.click(this.selector);
}
};
}
};

// ====================================================================================
// QUESTIONS – RETRIEVE INFORMATION from the system under test and the test environment
// ====================================================================================

// src/questions/CartItemCount.js
export const CartItemCount = {
value() {
return {
async answeredBy(actor) {
const cartBadge = await actor.ability.findElement(‘.cart-badge’);
const text = await actor.ability.getText(‘.cart-badge’);
return parseInt(text, 10);
}
};
}
};

// ================================================================
// TEST DATA FACTORY – Clean way to handle the Test Data generation
// ================================================================

// src/utils/TShirtFactory.js
export const TShirtFactory = {
create(color, size) {
const id = `tshirt-${color.toLowerCase()}-${size.toLowerCase()}`;
return {
id: id,
name: `T-Shirt Model ${color} ${size}`,
color: color,
size: size
};
},

createDiverseSet(count) {
const colors = [‘yellow’, ‘black’, ‘blue’, ‘white’, ‘green’, ‘turquoise’];
const sizes = [‘small’, ‘medium’, ‘large’];

return Array(count).fill(null).map((_, index) => {
const color = colors[index % colors.length];
const size = sizes[index % sizes.length];
return this.create(color, size);
});
}
};

Zusammenfassung

VORTEILE

  • Hoch wartbar
  • Multi-Interface-Unterstützung – kombiniert nahtlos UI-, API- und Datenbankinteraktionen
  • SOLID-Designprinzipien – folgt Single-Responsibility- und Open-Closed-Prinzipien
  • Geschäftsorientierte Sprache – Tests lesen sich wie User Stories/Workflows
  • Bessere Abstraktionsschichten – Tasks, Actions, Questions bieten klare Organisation
  • Hervorragende Skalierbarkeit – bewältigt komplexe Multi-Akteur-Szenarien gut
  • Wiederverwendbare Komponenten – Interactions und Tasks können kombiniert und wiederverwendet werden

NACHTEILE

  • Steilere Lernkurve – zunächst viel komplexere Konzepte zu verstehen
  • Begrenzte Community-Kenntnisse – weniger Praktiker mit praktischer Erfahrung
  • Erfordert stärkere technische Fähigkeiten – Team benötigt Verständnis von Designmustern
  • Überdimensioniert für einfache Projekte – unnötiger Overhead für kleine und sogar mittelgroße Anwendungen
  • Framework-Abhängigkeit – erfordert typischerweise spezifische Frameworks (z. B. Serenity/JS, Boa Constrictor, ScreenPy)
  • Längere Einarbeitungszeit – neue Teammitglieder brauchen länger, um produktiv zu werden

Den richtigen Ansatz wählen

Hier sind meine Empfehlungen für verschiedene Projektumgebungen. Als allgemeine Faustregel gilt: Am KISS-Paradigma (Keep It Stupidly Simple) festhalten und in den frühen Phasen kein Over-Engineering betreiben.

💡 Ich verwende den Oberbegriff Test-Maintainer, da das Test-Framework gleichermassen von QA Engineers, technischen Architekten und Softwareentwicklern gewartet werden kann.

Das Screenplay Pattern ist ideal für größere Teams, da es in komplexen Umgebungen hocheffektiv ist und eine stärkere Einhaltung von Programmier-Best-Practices sicherstellt.

Projekttyp

Anzahl Test-Maintainer

Empfohlene Strategie

Klein und einfach

Klein (1–3)

Page Object Model (Erweiterung zum Flow Model, wenn die Code-Wartung unhandlich wird).

Mittelgroß und komplex

Klein (1–3)

Page Object Model mit dem Gedanken an eine Erweiterung zum Flow Model, sobald die Bereiche identifiziert wurden, die davon am meisten profitieren würden.

Groß und komplex

Klein (1–3)

Page Object Model mit dem Gedanken an eine Erweiterung zum Flow Model, sobald die Bereiche identifiziert wurden, die davon am meisten profitieren würden. Wenn das Team wächst und ausreichend technisches Know-how vorhanden ist – schrittweise das Screenplay Pattern neben dem bestehenden Framework einführen.

Groß und komplex

Groß (mehr als 4)

da es in komplexen Umgebungen hocheffektiv ist und eine stärkere Einhaltung von Programmier-Best-Practices sicherstellt.

Bei Fragen gerne einen Kommentar hinterlassen.

Danke und viel Spaß beim Programmieren!