Behat Page Object Extension

A Behat extension that brings the Page Object pattern to your BDD tests, auto-injecting typed page and element objects into context step definitions.

Library
Composer
vv2.3.7
116stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
42/100Fair
Development Activity0
Maintenance20
Community76
Maturity60
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
74/100Good
Architecture78
Code Quality72
Innovation62
Learning Curve85

Behat Page Object Extension is a small, focused Composer package that wires the classic Page Object design pattern into Behat’s dependency-injection container. Instead of scattering CSS/XPath selectors and navigation logic across step-definition files, you extend the library’s base Page and Element classes (built on top of Mink’s DocumentElement and NodeElement) to encapsulate a page’s URL, structure, and behavior in one place, then have those objects handed to your Behat contexts automatically.

The extension registers itself as a Behat Testwork extension, loading a Symfony DependencyInjection container that provides a configurable page-object Factory, a class-name resolver for mapping short names to fully-qualified page/element classes, and an argument resolver that injects Page/Element instances into context methods purely from PHP type-hints. An optional lazy-loading factory, built on ocramius/proxy-manager, wraps page objects in proxies so referenced sub-pages aren’t eagerly instantiated until they’re actually used.

It has been maintained by SensioLabs and the Behat community since 2012, with the current major version supporting PHP 7.2 through 8.2 and Behat 3.8+. Development has slowed since its last release in January 2023, but the package remains a stable, widely-used building block for teams that want a maintainable Page Object layer without hand-rolling their own Behat integration.

What You Get

  • Base Page and Element classes - extend Page (built on Mink’s DocumentElement) or Element (built on Mink’s NodeElement) to model a page or a reusable UI fragment as a first-class object.
  • Automatic context injection - a PageObjectArgumentResolver injects Page/Element instances directly into your Behat step-definition method parameters based on their type-hints, no manual factory calls needed in most cases.
  • Configurable namespace resolution - map short page/element names to fully-qualified class names via a class_name_resolver, so step definitions can reference pages by short name instead of full namespace.
  • Inline elements - declare cheap, closure-free sub-elements as a $elements array keyed by name directly on a parent Page or Element, without creating a dedicated class for every fragment.
  • Lazy-loading proxies - opt into a LazyFactory (via ocramius/proxy-manager) so page objects referenced by a page aren’t instantiated (and don’t hit the DOM) until a method on them is actually called.
  • URL and response verification - Page::open()/isOpen() visit a page, then verify the HTTP response status and the resulting URL before handing control back to your test, raising a typed UnexpectedPageException on mismatch.

Common Use Cases

  • Login/logout flows - encapsulate a login form’s fields and submit behavior in a LoginPage object so every context that needs to authenticate calls $loginPage->login($user, $pass) instead of repeating selectors.
  • Multi-step checkout or wizard testing - model each step of a wizard as its own Page, using getPage() to move between them, keeping each step’s assertions and interactions isolated.
  • Reusable navigation/header components - define a site’s navigation bar as an Element referenced from every page object, avoiding duplicated selector logic across page classes.
  • Parameterized URL testing - use Page::open(['id' => 42]) against a path template like /employees/{id} to visit dynamic routes without manual string interpolation in step definitions.
  • Large BDD suites needing shared UI abstractions - centralize DOM knowledge behind page objects so refactors to markup only require updating one class instead of every .feature file’s step implementations.

Under The Hood

Architecture The extension registers as a Behat/Testwork ServiceContainer extension (PageObjectExtension implements TestworkExtension), loading services.xml via Symfony’s DependencyInjection component to wire a Factory (a DefaultFactory, optionally decorated by a LazyFactory using ocramius/proxy-manager for lazy-loading proxies), an argument resolver (PageObjectArgumentResolver) that auto-injects Page/Element objects into Behat context methods purely from type-hints, and an initializer (PageObjectAwareInitializer) that calls setPageObjectFactory() on any context implementing PageObjectAware. The domain objects are Page (extends Mink’s DocumentElement) and Element (extends Mink’s NodeElement), both implementing a marker PageObject interface; they expose getPage()/getElement()/hasElement() delegating back to the injected Factory, encapsulating navigation (open/isOpen), URL templating (unmaskUrl), and page-object composition, where referenced sub-elements can be dedicated classes or inline definitions keyed by name in a protected $elements array. Because Factory is an injected interface, swapping the resolution strategy (e.g. a custom class-name resolver) doesn’t touch any Page/Element subclass.

Tech Stack Built for PHP 7.2 through 8.2, layered directly on Behat 3.8+ and Mink 1.7 via friends-of-behat/mink-extension, using friendsofphp/proxy-manager-lts (a PHP 8-compatible fork of ocramius/proxy-manager) for lazy-loading proxies. Symfony components (DependencyInjection, Config, Yaml, Process, Filesystem, DomCrawler) wire the Behat extension itself and support the dev/test tooling; dev dependencies include phpspec/phpspec 6/7 for unit specs and behat/mink-goutte-driver plus fabpot/goutte for browser-driver-backed feature tests. The package is distributed via Composer/Packagist under the sensiolabs namespace, autoloaded PSR-4 from src/, with no build step required, and CI runs via GitHub Actions across a PHP 7.2-8.2 matrix with both high and low dependency-constraint variants.

Code Quality Testing is two-layered: PHPSpec specs under spec/ unit-test individual classes (PageSpec, ElementSpec, DefaultFactorySpec, and others) using Prophecy mocks and stubs, while .feature files plus bootstrap contexts under features/ run genuine end-to-end Behat scenarios against a small fixture PHP application, exercising the extension’s own DI wiring through itself. Error handling is explicit and typed via a small custom exception hierarchy (ElementNotFoundException, PathNotProvidedException, UnexpectedPageException) plus __call() throwing BadMethodCallException for undefined page/element methods, producing clear failures rather than silent no-ops. Naming is consistent (verifyX/getX/hasX conventions), classes stay small and single-purpose, and PHPDoc annotations — including modern @template generics — document types throughout in lieu of native return/property type hints, reflecting support for PHP versions that predate them. CI runs both suites across the full PHP matrix, but there is no static analysis (no PHPStan/Psalm configuration) and no code-coverage reporting configured.

What Makes It Unique The library’s specific contribution is bridging the well-known Page Object design pattern into Behat’s own dependency-injection container rather than leaving it to ad-hoc factory code in each project: it auto-injects Page/Element instances into context step-definition methods purely from PHP type-hints, supports both dedicated page-object classes and lightweight, closure-free inline element definitions on a parent page, and optionally wraps every page object in a lazy-loading proxy so navigating to a page doesn’t eagerly instantiate every sub-page it references. These are polished, practical conveniences layered on a widely-understood pattern rather than a novel testing paradigm.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search