Enemies
Fowler
I have chosen Python here, but this pattern applies in Java just as nicely. The best possible intro was written up by Martin Fowler. https://martinfowler.com/bliki/PageObject.html Fowler even mentions the selenium pattern implementation, with a note on assert style.
One of the biggest problems in any GUI type testing, is that often you need to visit a few different dialogues in an app to set up initial states. It is possible to use back-end API calls to set initial state too, if your context says it's better to do that, then do that. Something however, getting the browser and app to a point where a test is ready to start, becomes a mini-test in itself, or in unit testing parlance, a fixture. But before we go there, lets share my findings after a bit of Googling for better solutions and a good Python starting point.
Goucher
From the Pragmatic Programmer tutorial written by Adam Goucher. :
selenium_unittest.py:
def login(self): # set_text(), click_button() methods are extended methods in PageFactory self.edtUserName.set_text("opensourcecms") # edtUserName become class variable using PageFactory self.edtPassword.set_text("opensourcecms") self.btnSignIn.click_button()
Adam starts by fixing the bug in the selenium sample code by parameterising the login function in his post. There are a whole raft of tiny issues to be had with the selenium wrapper, mainly around extensibility, so I discarded it very early on.
Syntactic sugar
The first thing we see is that the PageFactory module assumes you initialise a member in your class called "driver" so it can talk to selenium. Fair enough. It also then wants you to add a dictionary of locators, which get automatically turned into class attributes using python __getattr__() . And finally you can add methods to your class as you expect to encapsulate a real world test. This sounds like a recipe that wants a bog standard base class.
I got disappointed by the module not addressing the issue the webElement interface set_text() method that for high-level use and simplicity really needs to implicitly do a clear() before the set_text() for readability. I addressed this by implementing a __settattr__() which does a clear() of the editbox, and send_text() all in one go. As you progress along, you will also find that sometimes you want to drive a element on the page which is not currently in view, the scrolling problem. Most of the time, you want the test to do what a human would do, scroll to the element and then interact with it automatically, inside the set()/get(), so that you don't litter your test code with scrolling code. There are many pitfals in web app automation using selenium that other higher level frameworks try to solve for you. ActionChains sound like the right solution to page scroll issues, but trust me, javascript to scroll to elements is more reliable.
a better login( )
def login(self, username, password): self.email = username self.password = password self.btn_login.click()
At a later stage I also started to follow a locator naming convention, and I swapped out "btn_login" in preference of a name-<type> and called it login_button.
And now out login() has parameterised instead of hard-coded the password here as Adam showed us already. I'm going to go farther than just the convenient login() function, because it deals with a fundamental difficulty with the locators collection and intellisense, the job of writing code without knowing the names of elements. I am going to suggest every form (class in Python) has a submit() method, for consistency, and then go on to suggest that every form also has a populate() function . This defends our tests, whenever the form starts getting busy or even fragile due to constant app under test change. Less repetition of test code that populates the form (DRY principle), improves maintainability and mostly test readability. But what, you might say about negative login tests? Like a blank password?
App conventions
Waits
The other thing I did in my page factory implementation is have the page object constructor wait for the page to load by waiting for all of the locators to be present instead of still requiring you write a WebDriverWait callable first, to handle the page load time problems all in one go.
So the rest of my page object is not really code, and looks like this:
class PageLogin(PageObjectBase): """ The new social login page """ locators = { "login_button": (By.XPATH, "//span[contains(.,\'Log in\')]"), "email": (By.ID, "email"), "password": (By.CSS_SELECTOR, "#password"), }
On the topic of locators, I found a small problem space with disabled and hidden controls that only appear later, or are dynamic, in cases where a page change state. Now you could argue for making separate pages when that happens, but it's illogical to do so in terms of mental overhead, code complexity and how to deal with backtracking of state.
<todo: show how we added a list of mandatory locators to deal with hidden ones at page load time>
Here is the code https://github.com/zaphodikus/PageObjectModel , this includes sample html pages, a mini Webserver and a few other tests to play with.
The real interesting part of this completely standalone test test suite is however https://github.com/zaphodikus/PageObjectModel/blob/main/PageFactory.py
This implementation copies 2 ideas from other implementations.
- element highlighting - a magenta drop-shadow gets added to every element we do a lookup on
- animation delay - a settable interval to slow down the automation so you can see what is happening
Enemy of scale
In summary
- easier to maintain code by using page object methods() to encapsulate common tasks like populating or submitting
- page load "wait" using the same locators.
- debugging by highlighting the interactive element
- locator naming conventions
- helper method conventions
- __getattr__ and __setattr__ to make managing the web mechanics easy
Next week in part 2 I'll cover the other parts of this standalone sample, but also the promised solution to simplifying the writing of workflow-like tests that consist of groups of pages that a tester wants to need to execute in a sequence to further remove duplication for common tasks in the web app. I hope this sharing of some code that's actually going in pattern at least, into a live production suite, will help people more than the simple homework answers that are shared in so many blogs.
(Code syntax highlighting done using http://hilite.me/ )
Comments
Post a Comment