Page objects in Behat

When working with web pages in behat, you can use something such as Mink directly to drive the browser and navigate around the site. But with page objects, you can make things a little more re-usable, here's how.

Let's say you had a product page, where you can click a button and add that product to the basket.

You might have a feature file like so:

  Scenario: Buying a single product
    Given there is a "Playstation 4", which costs £250
     When I add the "Playstation 4" to the basket
     Then I should have 1 product in the basket
      And the overall basket price should be £250

Under the surface, your When I add the "Playstation 4" to the basket might look like so:

/**
 * @When I add the :product to the basket
 */
public function iAddTheProductToTheBasket(Product $product)
{
    $productId = $product->getId();

    $this->visit("/product/{$productId}");
    $this->pressButton("add_to_basket");
}

This is assuming you have a transformer to convert the product from the product name to an entity. (I'll be covering transformers in another blog post soon)

This step definition works, and does what we need. But we might have a number of other scenarios that include steps to add to the basket, or via a different step definition we end up reusing things like $this->pressButton("add_to_basket"); - it feels a bit off to be re-using something like this, as if the ID of the button changes, we'll have to find-and-replace in our step definitions. Not a big job, but we can do better.

Page objects to the rescue

Page objects are a design pattern that allow us to create an object representation of a page on our website. With this, we can call methods on that object to interact with the page.

Page objects allow us to keep parts of our testing code re-usable too. They help with cleaner context files, better structured code, and can stop us needlessly repeating things.

Generally, page objects help abstract the UI away from our tests, as the tests interact with the page, where the UI logic is abstracted away.

There's a couple of page object extensions for Behat, for this, we'll be looking at the sensiolabs/behat-page-object-extension package, which has documentation available here.

With this extension installed and configured as per the instructions we can create a page object for use in our tests.

Implementing a page object

We'll need to create a page object that represents a page on our website:

features/bootstrap/Pages/ProductPage.php:

<?php

namespace Pages;

use SensioLabs\Behat\PageObjectExtension\PageObject\Page;

class ProductPage extends Page
{
}

To use this page, we need to inject it into a context we want to use it in:

features/bootstrap/Contexts/FeatureContext.php:

<?php

namespace Contexts;

use Behat\MinkExtension\Context\MinkContext;
use Pages\ProductPage;

class FeatureContext extends MinkContext
{

    /**
     * @var ProductPage
     */
    private $productPage;

    public function __construct(ProductPage $productPage)
    {
        $this->productPage = $productPage;
    }
}

This now gives us the chance to interact with our product page in our step definition via an object, rather than directly with the UI implementation.

Now, in our step definition, we can work with the page object instead:

features/bootstrap/Contexts/FeatureContext.php:

/**
 * @When I add the :product to the basket
 */
public function iAddTheProductToTheBasket(Product $product)
{
    $this->productPage->open(['id' => $product->getId()]);
    $this->productPage->addToBasket();
}

To call the open() method, we need to have a $path property on the page object:

features/bootstrap/Pages/ProductPage.php:

<?php

namespace Pages;

use SensioLabs\Behat\PageObjectExtension\PageObject\Page;

class ProductPage extends Page
{
    protected $path = '/product/{id}'; // the path of the page we're working with
}

Paths have parameters available to them if required, the parameters in the string of the path correspond to the parameters that are passed to the open() method on the page object:

Our {id} parameter in the $path is replaced with the value from $product->getId() from the line below:

$this->productPage->open(['id' => $product->getId()]);

Next, we can implement the addToBasket() method on the page object to allow us to add a product to the basket by clicking the button:

features/bootstrap/Pages/ProductPage.php:

<?php

namespace Pages;

use SensioLabs\Behat\PageObjectExtension\PageObject\Page;

class ProductPage extends Page
{
    protected $path = '/product/{id}'; // the path of the page we're working with

    public function addToBasket()
    {
        $this->pressButton('add_to_basket');
    }
}

Now, whenever we want to add a product to the basket, we have a re-usable way of doing so, where the UI is abstracted away. Now, if we needed to change the ID of the button, and the page was in use in multiple contexts, we'd only need to change the ID of the page object as that contains our implementation!

You can take this a step further, you can define individual elements on a page as objects and work with them if your page objects start to grow too big.

Supporting material