iTestBDD

Page Object vs Screenplay in BDD Frameworks

Most teams adopt the Page Object Model because it was the default recommendation in Selenium's documentation circa 2013. A decade later, those same abstractions are still running in CI — often unchanged — while the UIs they model have been rewritten twice and the test suite has grown to 800+ scenarios that take 22 minutes to run. The pattern didn't fail them; they stopped questioning whether it was still the right fit.

The core tension in BDD framework design is this: Given-When-Then syntax defines behavior at the domain level, but your step definitions execute at the UI or API layer. The design pattern you choose to bridge that gap determines whether your framework scales to 50 engineers or collapses under its own abstraction weight. Page Object and Screenplay are the two serious contenders, and they solve genuinely different problems.

By the end of this article you'll understand the structural trade-offs between the two patterns, see concrete implementation examples in Python (Behave) and TypeScript (Playwright + Cucumber-JS), and know which signals in your own codebase tell you it's time to migrate.

Master Modern API Test Automation

Hands-on courses in Python, BDD, AI-powered testing, APIs, and CI/CD automation.

Learn more

Given-When-Then Is Not a Testing Pattern — It's a Communication Protocol

The Given-When-Then syntax in BDD is a structured natural-language format for expressing system behavior as observable outcomes. "Given" establishes preconditions, "When" describes the action under test, and "Then" asserts the resulting state. It is not a unit test format — it is a contract between a product team and an engineering team, written in a dialect both can read. Cucumber-JVM 7, Behave, SpecFlow, and Cypress 13's built-in support all parse this same Gherkin grammar into executable step bindings.

Where BDD framework design gets complicated is the layer beneath the Gherkin. The scenario itself should be stable — tied to business rules, not implementation. The step definitions below it are volatile — they break when the DOM changes, when an API contract shifts, or when a new service gets introduced. The design pattern you use to organize that volatile layer is where Page Object and Screenplay diverge. If you're building a scalable BDD framework, this architectural decision is the one that compounds hardest over time.

Implementing Page Object and Screenplay Side-by-Side

The Page Object Model encapsulates a page's UI elements and interactions into a single class. Step definitions call methods on those objects. It works well for small, stable UIs with a clear page-per-feature mapping. Here's a Behave step using a Playwright-backed Page Object:

# pages/checkout_page.py
from playwright.sync_api import Page

class CheckoutPage:
    def __init__(self, page: Page):
        self.page = page
        self.promo_input = page.locator("#promo-code")
        self.apply_button = page.locator("[data-testid='apply-promo']")
        self.order_total = page.locator(".order-total")

    def apply_promo(self, code: str) -> str:
        self.promo_input.fill(code)
        self.apply_button.click()
        return self.order_total.inner_text()

# steps/checkout_steps.py
@when('the user applies promo code "{code}"')
def step_apply_promo(context, code):
    context.total = context.checkout.apply_promo(code)

@then('the order total should reflect the discount')
def step_verify_total(context):
    assert "$" in context.total

This is clean and readable — until you have 15 pages, shared components across pages, and a design system that moves a button between views. At that point, Page Objects start accumulating helper methods that belong to no single page, and your BasePage class becomes a junk drawer.

The Screenplay Pattern inverts the model. Instead of "pages that do things," you have "actors who perform tasks using abilities." Tasks are composable. Interactions are atomic. The same step definition reads identically, but the underlying structure is built from small, reusable units rather than monolithic page classes. Here's the equivalent in TypeScript with Cucumber-JS and Playwright:

// abilities/browse-the-web.ts
import { Browser, Page } from '@playwright/test';
export class BrowseTheWeb {
  constructor(public page: Page) {}
  static using(page: Page) { return new BrowseTheWeb(page); }
}

// tasks/apply-promo-code.ts
export const ApplyPromoCode = (code: string) => ({
  performAs: async (actor: Actor) => {
    const { page } = actor.ability(BrowseTheWeb);
    await page.locator('#promo-code').fill(code);
    await page.locator("[data-testid='apply-promo']").click();
  }
});

// steps/checkout.steps.ts
When('the user applies promo code {string}', async (code: string) => {
  await actor.attemptsTo(ApplyPromoCode(code));
});

The payoff is composability. ApplyPromoCode can be reused inside a larger task like CompleteCheckoutWithDiscount without duplicating locators or page-level state. One team migrating a 600-scenario Cucumber-JVM 7 suite from Page Objects to Screenplay reported their step definition file count dropping from 47 to 19, with run time dropping from 18 minutes to 4 after the refactor exposed enough duplication to enable proper parallel sharding. For a detailed comparison of when each pattern earns its place, the Page Object vs Screenplay decision guide covers the full trade-off matrix.

Choosing Between Them

  • Use Page Object when your UI maps cleanly to discrete pages, your team is small, and the framework needs to be approachable to engineers who don't own it full-time.
  • Use Screenplay when multiple personas interact with the same UI (e.g., admin + customer + API consumer), when tasks are shared across feature areas, or when your step definitions have started importing from each other.
  • Use Playwright over Selenium 4 for new projects — auto-waiting, built-in network interception, and native TypeScript support eliminate entire categories of flake. Use Selenium 4 when you need cross-browser coverage on legacy IE/Edge environments or have existing Grid infrastructure that isn't worth replacing.

Where Senior Engineers Still Get the Abstraction Wrong

The most common mistake is leaking UI vocabulary into Gherkin. Steps like When I click the "Submit" button on the checkout form bind your scenario to a specific implementation detail. When the button becomes a keyboard shortcut or an API call in a headless flow, the scenario breaks — not because the behavior changed, but because the step was written at the wrong abstraction level. This happens because step definitions are written by engineers who are thinking about the page, not the behavior. The fix is a naming review: every "When" step should describe a user intent, not a UI gesture.

The second mistake is storing UI state in the BDD context object (Behave's context, Cucumber's World). When a step sets context.page_object = CheckoutPage(driver) and a later step reads from it, you've created implicit ordering dependencies between steps. This is a scenario isolation problem that compounds badly as the suite grows — and it's one of the same failure modes that appears in AI-generated step context bleed. Use dependency injection (Cucumber-JVM's Picocontainer, SpecFlow's built-in DI) to scope state to the scenario, not the runner.

Myths That Still Shape Framework Decisions in 2024

Myth 1: BDD is a testing methodology. BDD is a collaboration and specification methodology. The tests are a by-product. Teams that adopt Gherkin purely as a test-scripting language — without the three-amigos conversation that produces the scenarios — end up with verbose step definitions that duplicate unit test coverage and add no specification value. If your product managers have never read your feature files, you're not doing BDD; you're doing Cucumber. Myth 2: The Page Object Model is the "correct" pattern for BDD. It's the most documented pattern, not the most correct one. The Screenplay Pattern has better SOLID alignment — particularly single responsibility and open/closed — and scales better in multi-persona test suites. The reason most teams default to Page Object is familiarity, not fitness.

Myth 3: 100% step coverage means good BDD. Coverage of step definitions says nothing about whether your scenarios describe meaningful behavior. A suite with 400 scenarios that all test the happy path through the UI is less valuable than 80 scenarios that cover critical business rules including edge cases and failure modes. Before adding more scenarios, audit what you have — tools like ChatGPT or Claude can surface coverage gaps quickly when given your feature files as context, and there's a structured approach to doing that in the ChatGPT coverage audit workflow. Density of meaningful scenarios beats raw scenario count every time.

If your step definitions are importing from each other, your Page Objects have a BasePage with more than five methods, or your Gherkin contains UI element names, those are the three signals that your framework design needs a structural review — not more scenarios. Start by extracting one high-traffic user journey into Screenplay tasks and measure the step definition reuse ratio before committing to a full migration. The next thing worth measuring after that is mean-time-to-detect on flaky scenarios at the step-definition layer.

Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.

Understanding how systems actually work is the first step toward navigating them effectively.

Browse all articles