A case against Page Object Model — part2
In a previous article a laid out a theoretical case against page object model and why you shouldn’t use it.
In this follow up article I’ll tackle few topics:
- Give a reply to some of the critics to the previous article
- Provide an alternative to Page Object Model
- Give a concrete example
1: Reply to critics
You picked a simple example.
One critic is that in my examples of LoginPage and HomePage, there’s no internal state, but there could be some other cases when the page object class does hold an internal state.
My reply is that in most cases, the overwhelming majority of the times, page objects are stateless.
So going through the trouble of creating all of these page objects for the rare cases when they hold an internal state (let alone the cases when the internal state is actually relevant to the test case) is too much of an overhead.
In addition to that I think it’s a bad practice to hold internal state that effects code execution path for automated tests, as this is prone to the nasty test smell of Conditional Test Logic that has the potential to induce test flakiness and unclarity.
You picked a dynamically typed language
The argument here being that statically typed languages (Java, C#) have more safe guards on the code.
Such that for example when we return a new page object from a method, this insures that the test code will take this object and operate on it’s methods accordingly.
This way we can avoid mistakes by the test code writer.
In a dynamically typed language like python or Javascript, this advantage is obscured since there aren’t as many safe guards over object types.
My reply is two folds:
- This is not true, python (similarly to most dynamically typed languages) has a static type checker.
So the alleged benefit can be achieved in both cases. - I still think that constraining the test script coder in such ways makes no sense.
Because more often than not, there will come a moment when the constraint doesn’t fit the test case, and now the test coder is reluctant to find work arounds to the imposed barrier.
Imposing limitations that don’t fit the test case not only take longer to conceptualize and develop (analysis paralysis), but they lead to inconsistent code that is more difficult to understand.
PageFactory and Screenplay
I was presented during the discussion with 2 additional design patterns that could be regarded as advantages of POM.
The first one is the PageFactory pattern.
It is internally provided by selenium web driver to achieve 2 main things:
- It eases on the writing of the page object model.
- It provides (at least in java language) out of the box solutions to various problems, for example, stale element exceptions are avoided by “pinging” the element again and again before it’s operated on.
The second pattern is Screenplay, which I have to admit, I was unaware of.
This design pattern subjects the page objects to the BDD format with the existence of “actor”.
The actor effectively orchestrates the entire test code, and provides an object through which all actions are handled (acting, querying etc).
My reply in general is that adding more design patterns is pretty much a dead give away.
When we pad our design with more and ever growing bandages, this should raise all of our red flags that maybe our initial idea doesn’t fit the problem.
We software developers, we are men and women of principles, we love our principles, we live for our principles and we will die for our principles, and if you don’t like our principles — we’ve got more principles.
PageFactory does provide a nice cover for page objects, as it deals with many of the pain and flakiness related to UI tests.
However it was designed under the assumption that page objects are the way to go.
It could do the same thing in a straight procedural style in a similar fasion with Playwright or Cypress.
And as to Screenply, I think this is way too much conceptual overkill that wouldn’t pay off.
Not only your padding your automation code with more “things”, abstract entities that readers would have to understand, you’re not really solving the problems of page objects but rather covering them up with a happy face.
Page object is a nonsensical class, the solution to nonsensical class cannot be adding more nonsensical classes…
Page Object is great if done correctly
Yeah, if you do what works it will work…
Tautological statements like that really reflect the circular reasoning behind page object models, if it doesn’t work for you, you’re not doing it right.
2: What’s the alternative?
Unfortunately, I feel like when I was asked for an alternative, I wasn’t asked for a good way to structure test code but rather to provide a new buzzword.
If I’m telling them not to use page object, then I must suggest some other design pattern or some mysterious ninja technique.
No, I don’t have a new buzzword — I think the last thing we need is a new buzzword.
What I offer instead is a straight procedural code.
Use functions to commit your high level actions and constants to store your locators, split them up to modules (or namespaces) when the need for that arise.
This way you don’t need to wonder about the purpose of these various objects and “things”, and access to locators is now more directed so that when locators are shared between different pages you don’t have to criss-cross your code to pave your way from one class to another.
OOP marries operations with data; and it’s not a happy marriage.
Because in the end, you end up with those giant objects with a lot of unrelated data in them that you poke in different parts of your software
— Stoyan Nikolov, OOP is Dead, Long Live Data-Oriented Design
3: Example
Lets take the original example and re-write it in a procedural style
Recap:
re-write into procedural style:
In the procedural style we don’t need to wonder about the significance of any class.
We don’t have to do silly things like creating a whole instance of a class call a few methods and throw it away.
We don’t need to ask philosophical questions about the nature of the pages and what purpose do they serve.
We can have a direct access to all locator at any given moment so that if some elements gets shared by several pages we don’t need to poke holes in our design to access them.
We can split our units of codes when we feel that it’s necessary instead of speculating how the different parts of codes will be wired up in the future.
Our code base is flatter and more direct instead of hierarchical and overly indented.
Just to clarify, I am not entirely against the idea of classes.
My test code for example is nested inside a class.
I think that classes can provide an important solutions for some problems, when some data fields and behaviors are closely aligned with each other.
In the test code, the behavior of the code is entirely intertwined with the state of the driver, hence the need to encapsulate the two together in a class and ultimately as an object.
But trying to shove every single part of your automation code into a class, the attempt to pad it up with more “actors” and “doers”, and speculatively trying to anticipate future needs, leads to those problems I mentioned.
I think that the default mode should be to write your code in a plain procedural style, and when cases do arise, when you found that some aspects of your code are coupled and should be encapsulated — by all means do it.
But don’t try to shove all of your behaviors into a page object and expect that to add clarity to your code, more often than not it will do the exact opposite.
Conclusion
Page object models virtues can be achieved in procedural style.
Classes and objects are just tools in the toolbox and they are ought to be used when needed, not just because they simply exist.
More specifically, classes are useful for encapsulating state with behavior and these are the cases when classes should be used.
When you find your code polluted with objects that don’t have internal state and you are merely creating the an object, use some of it’s methods and then dispose of it, consider refactoring these methods into functions.
I’ve got some more cool articles!
- Browser automation with robotframework
- Structural vs Nominal subtyping in python