Page Object Pattern

As important and critical unit tests are to our applications, functional testing of the front part should be a must part of any project too. At Accesto we use a combination of Selenium browser automation tool, Facebook's WebDriver and PHPUnit framework to make sure the user interface is working as expected from the day one.

Each integration test covers up to multiple scenarios and consists of series of standard interface actions like URL navigation, clicking on an element, button typing and alike. The WebDriver offers a variety of ways to select an element to act upon. It includes selection by XPath, by link text, CSS selector and so on.

However, as the tests grow in numbers, it can quickly become quite cumbersome to maintain test quality as they become fragile to any changes in the document structure. A simple change made by the frontend developers can lead to lots of failing test cases and time wasted looking for the answer what did fail and why.

There is a pattern though called the Page Object pattern that aids in creating clean and maintainable tests. Quoting Martin Fowler, the class implementing this pattern acts as a wrapper to any part of the web application, centralizing document querying, actions, etc. in an object-oriented manner.

IMPLEMENTING PAGE OBJECT IN PHP

We start by creating the base class for every Page Object:

<?php
namespace Accesto\Tests\Functional;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverExpectedCondition;
abstract class BasePageObject
{
    protected $driver;
    public static $baseUrl = 'http://dockerwww:8080';
    public function __construct(RemoteWebDriver $driver)
    {
        $this->driver = $driver;
    }
    abstract public function navigate(): self;
    protected function get(string $path): self
    {
        $url = self::$baseUrl . $path;
        $this->driver->get($url);
        $this->driver->wait()->until(WebDriverExpectedCondition::urlIs($url));
        return self;
    }
    protected function click(WebDriverBy $element): self
    {
        $this->driver->wait()->until(WebDriverExpectedCondition::elementToBeClickable($element));
        $this->driver->findElement($element)->click();
        return self;
    }
    protected function fill(WebDriverBy $element, string $value): self
    {
        $this->click($element);
        $this->driver->findElement($element)->sendKeys($value);
        return self;
    }
    // more helper methods
}

and very first FrontPage page object representing the landing page with the login form on its top:

<?php
namespace Accesto\Tests\Functional\Front;
use Accesto\Tests\Functional\BasePageObject;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
final class FrontPage extends BasePageObject
{
    public function navigate(): self
    {
        return $this->get('/');
    }
    public function loginAs(string $username, string $password): self
    {
        return $this
            ->fill(WebDriverBy::cssSelector('input.username'), $username)
            ->fill(WebDriverBy::cssSelector('input.password'), $password)
            ->click(WebDriverBy::linkText('Login'));
    }
}

Such object can be reused across many tests. Any change to the underlying document structure would be limited to this single class.

<?php

use PHPUnit\Framework\TestCase;
final class SecurityTest extends TestCase
{
    public function testItAuthenticatesUser(): void
    {
        (new FrontPage($this->driver))
            ->navigate()
            ->loginAs('[email protected]', 'secretP4ssw0rd');
        $this->driver->wait()->until(WebDriverExpectedCondition::urlContains('/my-account'));
    }
}

Remember, tests are as important as application code base. We want both to be maintainable, extendible and reusable as much as possible. Page Object pattern helps us to achieve that goal.

icon

Ready to make your SaaS Scalable?

Fix most important issues within days from the kick-off

CONTACT US

Related posts