If enough people ask for background, I’ll make a separate explainer thread about page objects and fluent test writing style. But for clarity I’m wanting to stick to the meat of it.
In POM (Page Object Model) speak a page object is an object that can be used to manipulate the UI of an app, where named instances of the object represent each of the possible dialogs in the app. The model is the way we can work with app dialogs as if they are interchangeable and each object to represent a page is built at runtime using a unique class for each page/dialog. Firstly when instantiated, the page object construction must always return an instance of the object that will drive the exact page and the exact page only. The constructor will thus wait for the page to complete loading and will assert (since constructors cannot return values) after a time is it cannot.
If a button in my app can end up moving us to one of 3 different pages based on situation, what is the best way to represent that in a page-object. For example :
- The user pressed “login” but the login server is unavailable they will see an error message and troubleshooter
- The user elsewhere entered a valid username, they will see the app main page
- The user is granted access but only if they provide an additional 2FA token because their last authentication was 7 days ago
If we are writing fluent code here we will go
startp = StartPage(driver)
homepage = start.press_login()
but since there are 3 possible outcomes I really need to write
homePg = start.press_login_expect_home()
troublePg = start.press_login_expect_trouble()
twofactorPg = start.press_login_expect_token()
And this starts to create messy use of the object, and flow hiding problems, because I really want to pass a delegate to the press_login() function, where the delegate is actually the constructor of the object I expect, and thus remove any knowledge from the button handler. Am I wrong in wanting to stick to my guns on keeping each page single purpose and clean at the cost of having less “fluency”?
I’d say to not represent the 3 different states.
If I were writing UI tests using a page object model and fluent code I’d want to stick to the ‘happy path’ only where possible (end to end/thin layer of tests at UI level). I can see how you’ve come to the question of 3 outcomes but error messages, sad paths etc I’d look to see if there were opportunities further left to test ‘the other stuff’. For example, I’ve seen many people write both happy path as well as then testing error messages, text, back links etc etc… instead I’ve tried to encourage users to automate a thin layer of end to end tests at the UI level (using POM/fluent) and then things like text/errros test at a lower level for example testing KVPs, unit testing views etc. That means the POM/fluent is easier (you know the page to return).
Not sure if that helps (sorry in advance - typing a response on my phone as the question was interesting )
Wouldn’t a fluent interface look more like?
homePg = start.inputUsername(username).press_login().expect_home()
troublePg = start.press_login().expect_trouble() // not sure how you're making the login server unavailable
Where each of the methods is returning the current instance of the page, allowing them to be chained?
Yes @vivrichards and yes @ernie
I’m glad my question was interesting, and like I said, people who are confused by it are welcome to ask for clarification. But good to see people who grok the problem. I had a question about this from one of my devs yesterday and it took me an hour to just rewrite 2 lines of heavily visited code. Which prompted me to start writing some better documentation on how the pattern works.
Agree Viv. My experience with page-object is that people love to talk about it in conferences (and this opinion extends to other fancy DDT frameworks too) and make it all out to be a great tool for quickly writing loads of tests. But then fail to explain that negative paths and branches are poorly handled by the framework, or just look ugly in it. It’s always easy to automate happy paths, and any tool that is making it hard to automate failure paths using that tool is a good reason to bin the tool. Customers stop using your app once your app stops handling failures gracefully. So yes. I changed the code so that the object does just one thing. Presses the button
So yes I’m going to now dig into Ernies response. Because I like it. I’ll explain why.
It means I have to create a parent or base class for the page objects and implement the
expect_xxx() functions in the base class
and then make the press_login() button merely return the this/self object
It does mean that my base class needs to know how to instantiate children, but that’s not a problem for circular dependencies if you are using a factory pattern.
The pain with factory pattern is that the interpreter you use becomes unable to divine the object “shape” or type anymore unless you are in a runtime, so intellisense or duck typing just does not work. And although people may groan about people duck typing, it’s my opinion that it aids learning and reduces errors. So you want to bear that in mind about object factory and your language implementation specifics. I’m intentionally not getting python-specific, because although it’s my wheelhouse, yours will differ.
So thanks both for confirming, I’m on track, and I can now write this up in some docs for my developers. I can feel a blog post on the topic coming
You can use polymorphism, returning a generic object (e.g. Page) and in the callee, who knows the context, transform it into a specific object (LoggedPage, LoginPage, etc).
Or you can use reflection and pass the expected type as an argument (e.g. start.press_login(LoggedPage::class)) - the press_login function can throw an exception if it can’t build the LoggedPage object.
Yeah, although some languages run into cyclical reference problems when you do this. And was a source of general pain for me until I bit the factory-pattern bullet.
Hence part of my deeper question was about avoiding polymorphism hell, but keeping true to page objects always raising an exception to ensure flow control is always the job of the test-case, and not supposed to be the job of the person reading the code or writing a new test most of the time.
I’m a big fan of writing code that “forces” you to do healthy things, and so “fluent” and very clever polymorphism (the fact that java has even cooler polymorphism than python does also helps) is just a natural way to make that a bit easier.