Customising CodeceptJS E2E tests

Dominic Fraser
codeburst
Published in
10 min readFeb 14, 2019

--

Previously we looked at what end-to-end (e2e) acceptance testing is, the layers beneath the CodeceptJS e2e testing framework, and how to set up an app for testing in ‘How to Dockerize your End-to-End acceptance tests’.

The Dockerized example project given in that post focussed on the environment setup needed to run tests against different browsers.

This post will focus on examples of writing tests when the the core CodeceptJS API is expanded via:

  • Custom helpers, to talk directly to the chosen backend’s API
  • Custom I methods
  • Custom page objects and page fragments

For writing basic tests without deeper customisation CodeceptJS documentation and tutorial videos provide a great introduction.

All examples given in this post can be seen in this example repo. Customisation options were first shown to me by Nima Soroush (creator of Differencify for visual regression testing) and Laurence Hole, and thanks goes to them for the initial implementation of some of the examples below.

Basics Refresher

CodeceptJS provides a single high level API with which to write tests against multiple browsers. Different ‘helpers’ can be chosen to act as the backend to this API, for example Puppeteer or WebDriver, with the test writer not having to be concerned about the implementation details behind the exposed API. If only the high level API is used then if the chosen backend is changed the tests do not need to be updated.

How CodeceptJS communicates to different helpers is covered in the previous post’s ‘layers of testing tools

CodeceptJS tests are written in a scenario-driven, behaviour driven development (BDD) style, with an API language that is easy for non-engineers to understand and use.

A CodeceptJS test could look like:

Example of a basic CodeceptJS test

Documentation for CodeceptJS basics and individual Helper reference guides (for example WebDriver) has been updated lately and the BDD style makes it fairly self explanatory for simple tests.

Locators (CSS|XPath|etc) now also have more documentation and so will not be covered here.

Before moving on to test customisation options one great debugging and test writing tool will however be looked at: ‘ pause()’.

Pause()

pause() is a powerful tool for both debugging failing tests and for writing tests in an interactive manner.

By adding the simple pause() method inside your test scenario Codecept will stop the test execution and allow you to try different I commands in real time. This allows you to quickly experiment with different commands without continually having to relaunch a scenario.

This can be further expanded by combining with the Puppeteer Helper’s show: true to launch a local browser instance and execute the tests steps within it, so you can see them executed alongside their terminal output. Whether developing complex scenarios at greater speed, or identifying simple mistakes such as a failing I.seeElement('x') not being due to the element not loading but instead a typo in the locator used, this greatly helps expand understanding.

‘pause()’ and Puppeteer’s ‘show: true’ providing a visual of interactive real time execution

When using the show: true entry in a Puppeteer helper it must be noted that Chrome must be installed on the developer’s local machine, and that the test command must not be executed inside Docker. The app under test can be within Docker if desired, but docker exec -it npm run test:command would result in trying to launch Chrome inside the container, which is not visible to us. Instead the external npm run test:command will ensure it runs against local Chrome. The example repo contains the pictured use of pause() and the Puppeteer config that goes along with it.

Custom Helpers

I.x methods are passed from the high level CodeceptJS API to the helper config specified in the codeceptjs run command. These are then translated behind the scenes using the specified helper’s API to talk to the browser.

Helpers work behind the scenes to communicate with the browser

Abstracting this implementation away results in far less complexity, after the initial configuration of the helper no attention needs to be paid to which specific backend has been chosen.

Adding a reference to a custom helper

However, should the developer want to, CodeceptJS does allow for interacting with a helper’s lower level API. ‘Custom Helpers’ are used to achieve this, which can extend or overwrite CodeceptJS methods. We will look at two examples of this below.

Multiple custom helpers can be added by specifying them as pictured to the left in the test configuration. The name of the helper is arbitrary, used simply running a test to be able to list all helpers in use by name.

CodeceptJS exposes a I.amOnPage(url) method. In CodeceptJS’ source code we can easily see how this communicates with the Puppeteer and WebDriver helpers.

‘amOnPage’ Puppeteer and WebDriver implementations

What if we wanted to wait until the page was fully loaded before moving on to the next action?

Two options that exist are to either extend the CodeceptJS API with a new separate I.waitForPageLoad() method, or to override the existing amOnPage method.

Extend CodeceptJS API

The core CodeceptJS API provides methods that work across all helpers. If we are extending this API then we should also look at implementing a new method for multiple helpers (and if useful enough contributing a PR back to allow others to benefit).

We will add a waitForPageLoad method to Puppeteer and WebDriver.

After including the custom helper file in the configuration as shown above we can access each helper’s API via this.helpers.<helper-name>.

Reading the Puppeteer documentation we can see that a page object is exposed, and that it can listen for events via page.once(). One event that suits our purpose is load.

This is not exposed in the WebDriver API, so we need to be a bit more creative. By combining waitUntil with browser.execute we can get very close to Puppeteer’s once('load'). MDN informs us that readyState complete happens just before load , and so is suitable to use here.

We now can follow I.amOnPage(url) with I.waitForPageLoad(), a completely new method.

Example of waiting for page load after navigation

Overwrite CodeceptJS API

Less used, as to someone new contributing to the codebase it could be unexpected, but still possible, is overwriting amOnPage to change its functionality.

Looking at Puppeteer’s documentation we can see that page.goto() supports a timeout as well as waitUntil. In the slightly contrived example we show that we can add in defaults for both of these, that would then override amOnPage whenever the custom helper was included in a configuration.

As a challenge to you the reader, if you can find a good way of duplicating the functionality of waitUntil: 'networkidle0' (which watches amount of network connections) for WebDriver let me know!

As we can see Custom Helpers provide the ability to speak to the lower level APIs beneath CodeceptJS. This can be useful for very specific behaviour that is not (currently) supported directly in the CodeceptJS API.

Next we will look at extending the I actor functionality in ways that do not require lower level API manipulation, but rather just the addition of application specific logic in a reusable way.

Custom ‘I’ methods

A large amount of I base methods already exist (documented in the reference for each helper) which continue to be expanded. It is possible to include new self named I methods that are abstractions of a series of existing base methods.

This is different from Custom Helpers, where new base methods are created, as instead it is simply used to avoid code duplication via abstraction. This can also lead to more readable central test files: for a site with authentication I.loginAsJohn() communicates the multi-method step faster than the I.fillField‘s and I.submit that would be behind it.

Custom ‘I’ methods reference

The include section is used for this and, as seen later, for adding custom Page Objects (also used for simplification and reducing duplication, but focussing on interactions with a specific area of a page).

By using the I key CodeceptJS knows to interpret the referenced file as an extension of the I actor.

Let’s look at how a more complex custom I method can be set up to show a longer method that truly would not belong in full in a main test file.

The login example in the CodeceptJS documentation shows how multiple simple steps can be combined, but we will look at how for complex applications even page navigation may need some assistance.

We will use Skyscanner as the page under test, as it provides an example of complex URL recreation. From the homepage we can see that when we make a flight search we end up on a page with a URL that contains locations, dates, and multiple other query params. A test that includes navigating to this page must be able to pass in multiple parameters for different use cases, ensure that the flight dates are always in the future without manual updates, and that new test contributions are made easy by handing URI encoding of params. Certainly too long to want to write more than once!

Let’s break down what we see in the example repo:

Example of adding a ‘I’ method

Firstly we add to I by adding our method inside the CodeceptJS global actor. This also gives us access to other I methods using this.existingMethod. We create a goToFlightSearch method that takes an optional object as a parameter, performs some conversion, and executes amOnPage(url) on a Skyscanner flight search URL. The URL structure is valid at the time of writing, taken from making a search and identifying params matching the flight specified.

Great! So even without conversion we could see how for a different use case we could use this to hardcode a common URL so that it did not have to be entered multiple times, even just I.amOnGoogle() would work.

Here however we can see there is a lot more going on. Default origins and destinations are passed in, but could easily be overridden using I.goToFlightSearch({ origin: 'lax', dest: 'cdg' }).

We can also see that there are two internal helper methods outside of the export used to construct a valid URL.

Additional non specified query params can be passed in as an object, and all params are URI encoded and mapped from an object to a valid query string using the helper method encodeGetParams.

A future flight date is dynamically created so that tests do not need to be continually updated, and it is modified to fit the non-standard ‘YYMMDD’ structure used in the URL with the help of date-fns.

By using include to extend I we have taken a 30 line long navigation and abstracted it in a reusable way to be available as I.goToFlightSearch([options]) within any test.

Custom Page Objects and Fragments

The CodeceptJS docs explain this well, so here we will just briefly describe the difference to I methods.

Setting up a page object, including it, and using it

As we are not extending I we access it slightly differently, this time by assigning the Codecept actor to a constant.

We export an object that is then accessed in tests by the name given to its key in include.

By adding this as a named argument in a Scenario we then have access to the locators and methods defined within it.

This again gives a level of abstraction when interacting with a page, and means if any defined area were to change it would only need to be updated in one place. Page Objects and Fragments are composed in the same way, and are only semantically different. A Page Object refers to an overall page, whereas a Fragment would be a part of a page, and as such a Fragment has the convention of specifying its root locator in its export for use when using the within selector function.

Summary

Custom Helpers provide the ability to talk directly to each the underlying backend libraries to write custom methods if the CodeceptJS API is missing something that the backend library provides.

Custom I methods, Page Objects, and Page Fragments can be used to abstract functionality away from a main test file and provide reusability.

pause() provides a great debug tool, especially when paired with the Puppeteer Helper’s show:true.

Hopefully this gives some useful insight and examples for testing more complex applications in a clean way with CodeceptJS.

Thanks for reading 😀

Here are a couple of other things I’ve written recently:

--

--