Skip to content
Colin Wren
Twitter

Why You Should Modularise Your Automation Code

JavaScript, Testing, Automation, Software Development6 min read

russian stacking dolls
Photo by Julia Kadel on Unsplash

The app I’m currently building — JiffyCV, makes use of three smaller libraries that are combined to help the user complete their tasks:

  1. A React Native component library for all the UI elements
  2. A TypeScript library that contains all the classes and functions required to work with the data structures we’re using in the app
  3. A React based templating library that contains all the components we’re using to render the data structures into HTML and produce printable PDFs

Each library deals with a particular job and it’s the app’s job to hook the UI elements up to code that works with the data structures with the end goal of getting that data into a template and printed to a PDF.

This approach makes it easier to build complex apps by breaking the functionality required to achieve a set of tasks into smaller cohesive chunks that can be treated as separate code-bases and used across a number of applications.

The modularisation of code is a common development pattern with a lot of effort being taken to identify contextual boundaries in the code so each library cleanly separates the concerns of the functionality it represents.

Separation of concerns in automation code

There are a number of development patterns in automation that aim to create a clear separation of concerns, the most commonly used being the Page Object Model (POM).

The Page Object Model is a means of modelling a page (or screen) in an app so that there’s a single class who’s job is to handle interacting with that page, such as getting text from the page or clicking buttons when the user navigates to a new page; then another Page Object Model is used to interact with that page.

This makes it easier to separate the code for interacting with the page from the code to setup tests and to assert results as well as making it easier to re-use the code for interacting with pages across multiple tests.

The issue with the Page Object Model is that if there are a lot of shared elements across pages then there’ll be a need to create reusable functionality and unfortunately, the most commonly used way of tackling this is to use inheritance to define functionality once in a super class and this leads to a very messy code-base.

Atomic design looks to solve this by introducing a hierarchy of Page Objects with their own concerns:

  • Atom — The simplest element on the page such as a text block or button
  • Molecule — A collection of atoms, such as a text input comprised of label and input atoms
  • Organism — A collection of molecules and atoms, such as a form comprised of many text inputs
  • Template — An abstract collection of organisms, molecules and atoms used to help automate pages that follow a templated layout, such as pages that a CMS might render
  • Page — A collection or organisms, molecules and atoms, such as a login page with a form for the user’s credentials

The atom level does the majority of the heavy lifting with the higher levels using functionality implemented in the lower ones to achieve a defined state or carry out tasks such as reading that state.

This means that instead of having to build an inheritance structure to deal with a common button, the Page Object Model can instantiate one of the atomic classes to interact with that button and verify the changes made by that interaction with one interface.

Contextual boundaries of automation code

Atomic design works well to separate the concerns of the different atomic levels but even with such a powerful paradigm in place it’s still common to see an automation code-base is separate to the main application’s code and that attempts to do too much.

It’s not uncommon for an automation code-base to have:

  • Page Object Models (atomic or otherwise)
  • Code to interact with the system under test via the GUI
  • Code to interact with the system under test via an API
  • Code to interact with the system under test’s database
  • Code to generate test data
  • Code to setup tests (ranging from starting Selenium to full blown infrastructure code)
  • Feature files if using Cucumber
  • Step Definitions if using Cucumber
  • Test files if not using Cucumber
  • Code for test reporting

While it may seem that having everything related to testing in one place makes it easier to work with, I’d argue that this all-in-one approach makes it harder to work with.

Anything other than the page level atomic POMs, test setup, tests and test reporting code should live alongside the respective development code-bases and be imported into the automation code-base in the same manner that the development code’s libraries are imported into the app.

The automation code-base shouldn’t be a separate code-base and instead should live alongside the development code so that there’s only one code-base to build and test the app and the test code never has to fall out of sync with the main code-base.

The benefits of modularisation

As mentioned earlier the app I’m building pulls in three libraries for the UI components, data structures and HTML templates.

When developing each library I have a suite of unit tests and for the UI and templating libraries I have a suite of automated tests that ensure the components and templates are rendering data correctly and that the callbacks for interactions are firing correctly.

These automated tests can be run across a myriad of device and browser configurations to catch any bugs that might be present and prevent these from making it into the downstream app, but they also serve another purpose — they’re verifying that the atom, molecule and organism level Page Objects work correctly.

When the libraries are published to the package registry it’s not just the development code that’s published, I’m also publishing the page objects so that these can be imported in the downstream apps that use the library.

This approach means that the Page Object code required for automated testing in the app is the high level Page classes who’s responsibilities are calling the lower levels to interact with the page.

Thanks to the automated testing done in the libraries I know that those lower level Page Objects work, so if there are any issues with the automation then it’s likely to be in the higher level Page which makes things easier to fix.

If there is a bug in the lower level Page Objects then it’s also easier to fix as the tests being run in the libraries that they live in are more focused on interacting with the UI components that I can set up the specific state that causes them to fail.

Additionally, if one of the libraries pushes a change that would break the automation then I can use semantic versioning to indicate this even if the development code itself isn’t a breaking change, giving me protection from accidentally breaking my test code.

A real example

In order to sell the benefits of this approach I’ll go into further detail about the ReactJS based templating library and how I used selenium to test the templates and verify the Page Objects before using them in my React Native app to verify the content of a webview.

Testing the template library

In my app the user creates a CV by selecting a set of CV items they want to include from a list of experiences they’ve captured and then this set of items is used to render a HTML page using React inside of a webview to provide them with a preview of the CV.

In order to provide users with a level of customisation they can also select different templates, fonts and colours to use for their CV.

This being the case it made sense to break the templates out into their own code-base that could be pulled into the main app, as this allows for development of the templates without the need for all the React Native dependencies the app has and the templates can be tested in isolation, meaning that any bugs should be at an integration level.

As the templates are React components I’m able to break the template structure into smaller components and unit test the different props permutations easily while also writing atom and molecule level Page Objects for interacting with these components.

1import React from 'react';
2import marked from "marked";
3import {Heading} from "./Heading";
4import { getYearString } from "../../utils";
5
6function AwardItem(x, index) {
7 return (
8 <div key={`award${index}`} className="my-3 px-5" data-test-id='award'>
9 <h6 className="font-semibold" data-test-id="award-title">{x.title}</h6>
10 <p className="text-xs" data-test-id="award-subtitle">{getYearString(x.date)}</p>
11 <div className="mt-2 text-sm" data-test-id="award-summary" dangerouslySetInnerHTML={{ __html: marked(x.summary.replace(/\n/g, ' \n'))}} />
12 </div>
13 );
14}
15
16export function Awards({ data }) {
17 if (data.accomplishments && data.accomplishments.length > 0) {
18 const awards = data.accomplishments.filter(x => x.type === 3);
19 if (awards.length > 0){
20 return (
21 <div>
22 <Heading light title='Awards' />
23 {awards.map(AwardItem)}
24 </div>
25 )
26 }
27 }
28 return null;
29}
An example of the smaller React components used in the template

The final template component then passes props into the smaller components and acts in a similar manner to the organism level Page Object that I create along side it.

1export async function getAwards(client) {
2 const items = await client.$$('[data-test-id="award"]');
3 return Promise.all(items.map(async (item) => {
4 const titleEl = await item.$('[data-test-id="award-title"]');
5 const subtitleEl = await item.$('[data-test-id="award-subtitle"]');
6 const summaryEl = await item.$('[data-test-id="award-summary"]');
7 const title = await titleEl.getText();
8 const subtitle = await subtitleEl.getText();
9 const summary = await summaryEl.getText();
10 return {
11 title,
12 subtitle,
13 summary
14 }
15 })
16 );
17}
An example of the function used in the organism level POM to get the awards from the template

To test the template render in a browser I spin up a React development server and pass in data to render the template, then use the organism level Page Object in a suite of selenium tests.

1describe('template', () => {
2 let client;
3 beforeAll(async() => {
4 client = await remote({ capabilities: caps });
5 await client.url('http://localhost:8000/template');
6 await client.waitUntil(async () => {
7 const nameEl = await client.$('[data-test-id="cv-title"]');
8 return nameEl.isDisplayed();
9 })
10 })
11
12 afterAll(async () => {
13 await client.deleteSession();
14 });
15
16 it('Shows the awards', async () => {
17 const awards = await getAwards(client);
18 const cvAwards = cvData.accomplishments.filter(i => i.type === 3);
19 const cvDate = getYearString(cvAwards[0].date);
20 expect(awards).toHaveLength(1);
21 expect(awards[0].title).toBe(cvAwards[0].title);
22 expect(awards[0].subtitle).toBe(cvDate);
23 expect(awards[0].summary).toBe(cvAwards[0].summary);
24 });
25});
Example of a test for the template to get the awards

I keep the Page Object code in a module in the /src directory alongside the development code as this allows it to be included in the published package and I include the classes in the modules exported by the index.js file that is the package’s main entry point.

Testing the template in the app

In the app I test the CV preview by navigating to the screen that has the webview containing the rendered React template and I then switch the Appium context to that webview.

Once in the webview I then use the same pageObjects I used in my selenium tests in the template libraries code-base to query the CV preview and verify that all the expected data is present.

1import { CVEditorScreen } from "../pageObjects/pages/CVEditorScreen";
2
3describe('User can select CV colour', () => {
4 let client;
5 let cvEditor;
6
7 beforeAll(async () => {
8 client = await setUpTests();
9 cvEditor = new CVEditorScreen(client);
10 await cvEditor.init();
11 });
12
13 afterAll(async () => {
14 await client.deleteSession();
15 });
16
17 it('Shows the awards in the CV Preview', async () => {
18 const expectedAwards = [
19 {
20 title: 'An award',
21 subtitle: 'I did a thing',
22 summary: 'I did this thing and it got me an award',
23 }
24 ];
25 const awards = await cvEditor.getPreviewAwards();
26 expect(awards).toBe(expectedAwards);
27 });
28});
An example test using the getPreviewAwards to get the awards from the rendered template in the webview

The Page Objects in the app are light weight as they only have to handle finding the webview on the page and changing the context, at which point it treats the webview content as just another organism level Page Object.

1import { BrowserObject} from 'webdriverio';
2import { pageObjects } from "@scoped-package/templates";
3const { getAwards } = pageObjects;
4
5export class CVPreviewScreen
6 private preview;
7
8 constructor(private client: BrowserObject) {}
9
10 async init() {
11 const previewEl = await this.client.$('~CV Preview');
12 this.preview = previewEl;
13 }
14
15 async getPreviewAwards() {
16 let entries = null;
17 try{
18 await this.client.switchContext('WEBVIEW_com.myapp.myapp');
19 entries = await getAwards(this.client);
20 } finally {
21 await this.client.switchContext('NATIVE_APP');
22 }
23 return entries;
24 }
25}
Example Page level POM using the getAwards from the templates package

Summary

Using the Page Object Model and Atomic Design patterns is a good way to separate the concerns of your automation code-base, but as the system under test grows and the development team starts to split functionality into libraries you should look to move the lower level Page Objects into those libraries so that you can test at that level and re-use the code wherever that library is used.