Architectural Concerns of the Page Object in Automated Testing
A couple of months ago I moved from being a developer to being a tester on the project I am currently working on.
In the team the testers are writing an automation suite as well as manually testing regressions, although the latter happened more often due to changes in the app being made quicker than in the automation.
On joining the team my first role was to add test cases to the automation suite for some of these changes but it became apparent to me while doing this that the reason the suite struggled to keep up was down to the following reasons:
- No-one in the team was paying attention to the health of the automation suite, it had gone red a long time ago but more test cases were being built on top of it
- The automation suite was hard to maintain and keep green due to the code being built using the Page Object Model pattern but without clear architectural rules to back it up
- There was no formal branching, merging and code review process
- Due to the maintainability issues of the automation suite none of the developers wanted to be responsible for ensuring they updated when changing parts of the app
Having spent a number of years instilling code quality into the teams I’d been in I decided to call a meeting within the test team to work together to solve these issues.
Three main points came out of the initial meeting:
- In order to regain visibility of the health of the app we would focus on a smaller set of tests that we would keep green and refactor the failing tests over time
- A refactor of the automation suite was badly needed
- WebdriverIO, the automation framework being used didn’t give us as much flexibility as we would have hoped in regards to running Cucumber scenarios in our IDEs or just failing tests
Out of these actions we tackled points 1 and 2 first. By setting down clear architectural rules while completing point 2 we could make the conversion to a different framework (point 3) easier.
In order to refactor the code I suggested to the team we break the code (which had followed the Page Object Model pattern laid out in the WDIO tutorials) into smaller objects with their own concerns:
- Concerns itself with the selectors needed to access web elements for the different components
- Selector strings would be abstracted away into functions. This allows for more semantic naming of the different selectors as well as allowing us to consolidate multiple selectors that just vary by an index selector (such as nth-child) into one function with an index argument
- Concerns itself with components on a page. Different pages might use the same component
- Is the only point that uses selectors to get web elements. This prevents page objects bypassing the component when accessing elements
- Any web element and web element attribute getters and setters would be abstracted away into functions. This allows for more semantic naming of the different operations and allows the internal implementation to be changed easily
- Concerns itself with interacting with the different components that make up a page, there are no direct calls to elements on the page in the Page Object
- Complex actions are abstracted away into functions that result in idempotent operations. For example the login function would still log you in whether you had the login form open or not when calling it.
- Concerns itself with calling actions on different Page Objects to achieve the defined task in the Gherkin Step
This gave us a clear division to work with and allowed for more constructive code reviews to take place.
This also allowed for the Page Object Model code to be broken out into it’s own library that could be developed outside of the automation suite’s code base, which at that point was getting to be a labyrinth of various step definition files with different implementations in them.
I was assigned some time in our next sprint to do a quick spike to prove out the idea and it worked really well so we decided to go ahead and refactor the code base.
The main hurdles during the refactor were:
- What was considered a component and what the boundaries of the different components were. This was mostly due to the different implementations floating around the existing code base contradicting each other
- Members of the team falling back into old habits, although these were ironed out through code reviews
Change is inevitable
Once we had the automation suite refactored the project shifted focus from integrating with one set of systems to a completely new set of systems.
This change in systems meant that half the code in the automation suite was made redundant overnight. It was deemed this would be a good excuse to migrate from WebdriverIO and use CucumberJS and WebdriverJS.
This port was made easier thanks to the clear architectural boundaries we had laid out as we could re-use the Page Objects and most of the Component Objects didn’t need to change (aside from moving from the differences in the WebdriverIO and WebdriverJS syntaxes) with most of the effort taking place in the Selector Objects where the new UI had a different structure.
A couple of weeks later it was decided to expand the new WebdriverJS based suite to support the old functionality we had in the old WebdriverIO suite.
This inclusion of two different sets of components within the page object was initially seen as a hard problem to solve, until we had a quick team chat and realised that we could utilise a strategy pattern on the Page Object’s constructor to assign the relevant Component Object at runtime and this didn’t break the original architectural boundaries.
Outcomes and Lessons Learned
The outcomes of taking a step back, thinking about each Step Definition & Page/Component/Selector Object, ensuring that these architectural boundaries are not crossed through disciplined code review and have been highly positive for the team.
Like most changes there was a little bit of initial push back and team members just doing what they’ve always done, but having an agreed approach and with everyone keeping each other in line when it comes to sticking to that approach, these behaviours changed quickly enough.
The benefits of the architectural changes have been seen at least three times since we adopted them and I’m sure they will be seen again.
Lessons learned from this refactor:
- There is always enough time to take a step back and fix problems that are preventing the team from performing at their best
- Teams should treat any code, test or otherwise with the same amount of respect as they would production code
- Some people will push back because they don’t see the benefit of new approaches, but it might just be that they can’t see this until they get hands on experience and once they understand the rules in their own way they will see those benefits
- Listen to your team and take on new approaches together
- In a fast changing environment being loosely coupled is key
- As a tester you’re not just automating tests to make your job easier. You are doing it to ensure that all members of your team get value from the feedback these tests give
- WebdriverIO abstracts a lot of the Promise based operations away from the end user which is why I like it, but the Cucumber runner needs a lot of improvements to make it easier for testers to get more benefits from it.