Technology

How we chose a testing framework: Part 2

Go Back
Alan Ip

2 months ago

Continuing on from the previous post in the series, let’s look at the techniques we considered when starting to write our acceptance tests.

Planning ahead on how test scripts should be organised means they can be strategically built on top of each other, saving a lot of time and unnecessary repetition. In some respects, deciding on the architecture of the test suite can be as complex as deciding the architecture of the operational codebase itself.

We started by structuring our acceptance tests using Codeception’s “cest classes”, which gives us all the benefits associated with OOP, e.g. re-usability of common test logic, saving us both development time and maintenance effort too.

We also implemented a few design patterns recommended by both Codeception and Selenium.

Page objects

We used Page objects for splitting a full webpage by its view components. The purpose is to separate the “steps” of the overall acceptance test from the implementation of each component.

For example, the quicksearch input box may refresh results while the user types, or it may only show results after the user hits enter. However, as far as the acceptance test is concerned, it just needs to know that a search term was submitted and that results are displayed. The quicksearch input box would therefore be suitable for existing as its own (or part of another) page object.

This separation also offers the potential for different teams to “own” particular parts of functionality in future, e.g. one team may own the search functionality, whereas another team may own the functionality for categorisation or filtering of products on a listing page.

Each page object is defined in its own class.

In fact, to really encourage the usage of page objects, Codeception will automagically (via reflection methods) instantiate and dependency-inject page objects into your cest methods. All you need to do is define which page objects you want via the parameter list of the cest method – Codeception does the rest!

/**
 * Submit a search request via the quicksearch input box.
 *
 * @param \AcceptanceTester     $I    Acceptance tester
 * @param \Page\QuicksearchPage $page Page object for quicksearch
 */
public function submitQuicksearchRequest(\AcceptanceTester $I, \Page\QuicksearchPage $page)
{
    ... Do things with $page - it will automagically exist! ...
}

UI Mapping

We used UI mapping for separating the visual implementation of webpages (e.g. the CSS selectors for picking out a specific button on a webpage) from the functional logic (e.g. what happens after you click the button).

The purpose is to allow the freedom to amend and refactor the visual implementation (e.g. apply a platform-wide update to the CSS class that describes the “add to cart” button) without affecting the acceptance test logic (e.g. what happens when the “add to cart” button is clicked hasn’t changed).

The separation also makes changes to the visual implementation more easily accessible to those concerned. For example, frontend UI developers need not worry about accidentally altering the UX of the webpage.

We have defined UI mappings as their own individual “UI map” classes.

// ProductDetailsUIMap.php

/**
 * @const string CSS selector for the “Add to cart” button
 */
const ADD_TO_CART_BUTTON = ‘#product-page .add_to_cart_button’;
UI mapping, which contains only CSS selectors.


// ProductDetails.php

use ProductDetailsUIMap as UIMap;

/**
 * Click add to cart button.
 *
 * @param \AcceptanceTester $I Acceptance tester
 */
public function clickAddToCartButton(\AcceptanceTester $I)
{
    …
    $I->click(UIMap::ADD_TO_CART_BUTTON);
    ...
}

Test dependencies is another feature of Codeception worth mentioning, as it will quite likely crop up when writing a test that forms a part of a longer chain of events. One such area for bluCommerce is the checkout process: it requires products being in the cart before being able to access the checkout; and requires customer details being captured before being able to capture delivery details. Codeception handles this by supporting the @depends annotation against the PHPDoc of the test. For example:

/**
 * Add a product to the cart.
 *
 * @param \AcceptanceTester $I Acceptance tester
 */
public function addProductToCart(\AcceptanceTester $I)
{
    ...
}

/**
 * Login as via guest checkout
 *
 * @depends addProductToCart
 *
 * @param \AcceptanceTester $I Acceptance tester
 */
public function loginViaGuestCheckout(\AcceptanceTester $I)
{
    ...
}

With this setup, whenever a set of acceptance tests are run, Codeception will automatically determine the dependencies between them, and run them in an appropriate sequence.

Once we begin writing tests for real world applications, one of the first barriers we come across is in sourcing test data in a reliable fashion, so that we can pass tests – and faithfully. We’ll discuss in the next post of the series.