Why You Should Modularise Your Automation Code
The app I’m currently building — JiffyCV, makes use of three smaller libraries that are combined to help the user complete their tasks:
- A React Native component library for all the UI elements
- A TypeScript library that contains all the classes and functions required to work with the data structures we’re using in the app
- 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.
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.
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.
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.
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.
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.