Overview
This skill is a pure reference - no execution steps; it is the canonical catalog cited to determine "what good looks like" per pattern. The catalog complements test-code-conventions (which is file-level §1-§10) with the architecture-tier vocabulary.
When to use
Do not use this skill to:
Each pattern below states what it is: its definition, canonical citation, and load-bearing rules. Use the pattern selection matrix to choose one; the detailed per-pattern when-to-use rules and anti-pattern tables live in references/pattern-catalog.md.
Pattern 1 - Page Object Model (POM)
Canonical source: Martin Fowler's PageObject definition (opens in new window) (the bliki article is the cross-language canonical reference) + Selenium HQ documentation on Page Object Models (opens in new window).
Fowler's definition: "A page object wraps an HTML page, or fragment, with an application-specific API, allowing you to manipulate page elements without digging around in the HTML."
Selenium HQ's elaboration: "A page object is an object-oriented class that serves as an interface to a page of your AUT… There is a clean separation between the test code and page-specific code, such as locators."
The three load-bearing rules:
Pattern 2 - Screenplay
Canonical source: Antony Marcano, Andy Palmer, and Jan Molak - Serenity BDD documentation on Screenplay (opens in new window); origin article Marcano, Palmer, Molak and Smart, "Page Objects Refactored: SOLID Steps to the Screenplay/Journey Pattern," which traces the approach to Marcano's 2007 conception.
The Screenplay vocabulary (Serenity BDD docs (opens in new window)):
| Term | Definition |
|---|---|
| Actor | The user or system performing tasks. "In Screenplay we model actors who interact with an application in various ways to perform tasks that help them achieve their goals." |
| Ability | A capability that enables actors to perform tasks (e.g., BrowseTheWeb, CallAnApi). |
| Task | A higher-level domain concept that groups Interactions (e.g., Login, AddToCart). |
| Interaction | A low-level operation (click, type, fetch). |
| Question | A query about system state used in assertions (e.g., TheCartTotal.value()). |
Why Screenplay vs POM: Screenplay separates what the user does (Tasks, Interactions) from what the user can do (Abilities) from what the user observes (Questions). The result is a SOLID-aligned object model that survives UI refactors better than POM in large suites.
Pattern 3 - Component Object
Canonical source: Selenium HQ docs (Page Components are part of the official POM extension) + practitioner consensus (testing-library, Storybook, Playwright Component Testing). Treated as a refinement of POM, not a competing pattern.
Definition: A Component Object is a Page Object scoped to a UI component (header, nav, form, modal, card) rather than a whole page. Where a page contains a re-used component (the navbar appears on every page), the Component Object models that component once; each Page Object that contains it composes it in.
Pattern 4 - App Actions (Cypress idiom)
Canonical source: Gleb Bahmutov - "Application Actions: Use Them Instead of Page Objects" (Cypress blog, 2019) (opens in new window).
Definition: App Actions bypass the UI for setup steps by exposing application functions (Redux dispatches, store mutations, API calls) directly via cy.window().its('app') or equivalent. The test still asserts via the UI; only the Arrange phase is short-circuited.
Why App Actions vs POM: "Logging in" is not what the test is about - it's overhead. App Actions skip the login UI flow and inject a session directly, making the test 10× faster and removing flake from the login form.
Pattern 5 - Service Object
Canonical source: Ruby on Rails / Java enterprise testing patterns + practitioner blog consensus. Refinement of POM for non-UI test layers.
Definition: A Service Object is the API-test equivalent of a Page Object - it wraps a remote service (REST endpoint, GraphQL query, gRPC method, message-queue producer) with a domain API the test consumes. Methods like cartService.addItem(sku, qty) rather than httpClient.post('/api/cart/items', { sku, qty }).
Pattern 6 - Repository (test-data access)
Canonical source: Martin Fowler's Repository pattern (opens in new window) (originally for domain-driven design) adapted for test-data setup. Practitioner adoption in 2020+ test frameworks (factory libraries layer on top).
Definition: A Repository in test context is the data-access abstraction that hides the storage mechanism (DB, fixture file, factory call) behind a domain API: userRepo.createTestUser({ role: 'admin' }).
Pattern 7 - Screen Object (desktop / mobile sibling of Page Object)
Canonical source: Martin Fowler's PageObject article - the current bliki entry (opens in new window) defines it as an object that "wraps an HTML page, or fragment, with an application-specific API". The earlier name WindowDriver (Fowler, 2004) covered desktop GUI windows under the same encapsulation principle before the term migrated to web. The desktop / mobile community reuses the structurally-identical pattern under the name Screen Object (one class per logical screen, locators + actions encapsulated, no assertions inside). No single owner formally documents the rename - screen object is community-canonical across FlaUI, XCUITest, Appium / Espresso practitioner literature.
The mobile sibling is documented inside Google's Android testing guidance as Screen Robot (Jake Wharton - Instrumentation Testing Robots (2016) (opens in new window)) and inside Square's mobile literature as well; both reproduce the same encapsulation contract.
The three load-bearing rules transfer unchanged from POM:
Worked desktop example (FlaUI / xUnit)
Bad (mechanical leakage into the test body - same shape as the web POM anti-pattern):
[StaFact]
public void Logs_in_with_valid_credentials() {
var window = _fx.App.GetMainWindow(_fx.Automation);
window.FindFirstDescendant(cf => cf.ByAutomationId("Username")).AsTextBox().Enter("alice@example.com");
window.FindFirstDescendant(cf => cf.ByAutomationId("Password")).AsTextBox().Enter("hunter2");
window.FindFirstDescendant(cf => cf.ByAutomationId("LoginButton")).AsButton().Invoke();
Assert.Equal("Invoices", _fx.App.GetMainWindow(_fx.Automation).Title);
}Good (Screen Object at the business layer):
[StaFact]
public void Logs_in_with_valid_credentials() {
var login = new LoginScreen(_fx.App.GetMainWindow(_fx.Automation));
var main = login.SubmitsCredentials("alice@example.com", "hunter2");
Assert.Equal("Invoices", main.Title);
}The mechanics live inside LoginScreen (constants for AutomationIds, retry-wrapped element fetches, SubmitsCredentials returns the next Screen Object). The test reads as a specification.
Pattern selection matrix
The patterns are not equally good for every project. The matrix:
| Pattern | Best for | Avoid for |
|---|---|---|
| POM | Page-oriented web SUT, 3-50 engineers, classic frameworks | Component-first React/Vue (use Component Object); Cypress (consider App Actions) |
| Screenplay | Large suites (200+ tests), multiple actor types, SOLID enthusiasts | Small projects (overhead exceeds benefit); teams allergic to dependency injection |
| Component Object | React/Vue/Svelte component-architected SUT, Storybook-integrated | Server-rendered traditional pages (use POM) |
| App Actions | Cypress + Redux/store-architected SUT, setup-heavy tests | Critical-path / smoke tests (must exercise UI); SUT without programmatic state API |
| Service Object | API / integration / contract tests with 5+ services | UI-only tests (no service calls); contract tests via schemathesis (the tool generates its own client) |
| Repository | Multi-data-source projects, DB + fixture + factory in one suite | Single-source projects (overhead exceeds benefit) |
| Screen Object | Desktop / mobile SUT through any accessibility-tree backend (UIA, XCTest, AT-SPI, Appium / Espresso) | Pure web SUT (use POM); pure API tests (use Service Object) |
Cross-cutting anti-patterns (apply to any pattern)
| Anti-pattern | Why it fails |
|---|---|
| Mixing two object-model patterns in the same codebase | Engineers can't tell which to write; vocabulary drift accelerates |
| Inheritance hierarchies >2 levels deep (BasePage → AppPage → DomainPage → SpecificPage) | Depth-3+ chains break unpredictably on root-level changes (§A2) |
| Page / Component / Task / Service Objects holding mutable test data | Cross-test coupling; parallel-execution breakage |
Public getter-style methods that expose locators (get loginButton()) | Defeats encapsulation; locators leak into test code |
| Object-model methods that wait, retry, or handle SUT errors | Hides flakiness; tests pass when they should fail loudly |
| Object-model classes that import test-framework assertion libraries | Implies assertions are happening inside; smell for the no-assertion rule |
Hand-off targets
References
Canonical sources, each cited inline above at the rule it grounds:
object-model-patterns - when-to-use rules and anti-patterns
View source (opens in a new tab) (opens in new window)Per-pattern selection detail for object-model-patterns. Each pattern's definition, canonical citation, and load-bearing rules stay in SKILL.md, and the quick chooser is the pattern selection matrix there. This file carries the detailed when-to-use rules and the per-pattern anti-pattern tables.
Pattern 1 - Page Object Model (POM)
When to use POM
Anti-patterns (canonical)
| Anti-pattern | Why it fails |
|---|---|
| Assertions inside the POM | Couples the page model to test outcomes; reuse across tests becomes brittle |
void-returning navigation methods | Loses the compile-time check Fowler explicitly identifies as the pattern's benefit |
clickAddToCartButton() instead of addToCart() | Couples the test vocabulary to UI mechanics - when the UI changes, every test changes |
| Exposing the underlying WebDriver / Page instance through public POM methods | Leaks framework details into tests; defeats the encapsulation |
| One God-POM serving five pages | Violates single-responsibility; bigger refactor cost than the POM was supposed to prevent |
Pattern 2 - Screenplay
When to use Screenplay
Anti-patterns
| Anti-pattern | Why it fails |
|---|---|
| Mixing Screenplay and POM in the same codebase | Doubles the maintenance surface; engineers can't tell which to write |
| Tasks that do not call Interactions (Task = re-named POM method) | Loses the Screenplay benefit; the team got Page Object Model under a different name |
| Question classes that mutate state | Violates the Question's "pure observation" contract; assertions on observations fail unpredictably |
| Abilities used as a junk-drawer for utilities | The Ability should grant a real capability; using it as a service-locator defeats the dependency-injection benefit |
Pattern 3 - Component Object
When to use Component Object
Anti-patterns
| Anti-pattern | Why it fails |
|---|---|
| Modelling every DOM element as a Component Object | Component Objects are for re-used components, not every <div> |
| Component Objects that hold cross-component state | Violates the encapsulation; the component should not know which page contains it |
| Page Objects that bypass the Component Object and target its internals | The Component Object's locators get duplicated; refactor leakage |
Pattern 4 - App Actions (Cypress idiom)
When to use App Actions
Anti-patterns
| Anti-pattern | Why it fails |
|---|---|
| App Actions for the Act phase (the thing under test) | The test no longer verifies the UI path under test |
| App Actions that aren't documented as test-only surface | Production code accidentally depends on the test-only API |
| Mixing App Actions and POM without convention | Engineers can't tell which to use; the suite forks |
| App Actions for end-to-end smoke / critical-path tests | Critical paths must exercise the full UI; App Actions skip the very thing the smoke proves |
Pattern 5 - Service Object
When to use Service Object
Anti-patterns
| Anti-pattern | Why it fails |
|---|---|
| Service Object that re-implements the production service (mocks-in-disguise) | Tests against a fake instead of the real service; misses contract drift |
| Service Object with assertions inside | Same anti-pattern as POM assertions - couples model to test outcomes |
| Single Service Object for 10 different services | Violates single-responsibility; the object becomes a god-client |
| Service Object that handles retries / circuit breakers identical to production | Tests pass because the Service Object hides the failures the test should catch |
Pattern 6 - Repository (test-data access)
When to use Repository
Anti-patterns
| Anti-pattern | Why it fails |
|---|---|
| Repository that mixes test setup with production data fetching | Production code accidentally adopts test-only quirks |
| Repository methods that return mutable objects shared across tests | Test cross-coupling; one test mutates and breaks another |
| Repository that creates "magic" data the test doesn't see | Tests pass for inscrutable reasons; debugging is impossible |
Pattern 7 - Screen Object (desktop / mobile sibling of Page Object)
When to use Screen Object
Anti-patterns (Screen Object-specific in addition to the POM list)
| Anti-pattern | Why it fails |
|---|---|
Screen Object that hard-codes AutomationId strings inline in every method (e.g. cf.ByAutomationId("LoginButton") repeated) | Refactor cost when the developer renames the AutomationId; centralise the constant at the top of the Screen class |
| Screen Object that wraps a single accessibility-tree call without adding a domain method | Same anti-pattern as the POM clickAddToCartButton() smell - Screen exposes mechanic, not service |
| Screen Object that asserts on accessibility properties (role, label) it controls | Asserting on internal state defeats the no-assertions rule; assertions belong in the test |
Screen Object that calls Thread.Sleep / Task.Delay between actions | Hides flakiness; route through the driver's retry primitive (FlaUI Retry.WhileNull, XCTest waitForExistence) |
| Screen Object that depends on absolute window coordinates | Defeats the accessibility-tree abstraction; multi-monitor / DPI / locale breaks the test |
| One Screen Object class per dialog AND per main view in the same screen | Modal sub-screens are nested Screen Objects; do not flatten |