When we automate web UI tests, we need a way to model interactions with the pages under test. The page object pattern is one of the simplest and most popular ways to do so.
A page object is an object representing a webpage or component. They have locators for finding elements as well as interaction methods.
Page objects make low-level Selenium WebDriver calls so that tests can make short, readable calls instead of complex ones.
Our test interacts with two pages — the search page and the result page.
When I write tests, I like to take a top down approach by naming page object methods first and then implementing them with Selenium WebDriver calls later. Like I said before, think before coding.
The search page needs two actions —loading the page and entering a search phrase on the page.
The result page doesn't make any actions, but it does return three values — the result link titles, the search input value and the page title.
We'll write page object stubs for both pages and then use them to write our test case function.
Let's look at the code.
In our project, I've created a directory called
pages right here.
Notice that this directory is outside of the
pages could be used by things other than tests.
We also need it outside because we want this
pages directory to be a Python package.
Python packages are denoted by this
__init__.py file (pronounced “dunder init”). It's just an empty file inside of a directory that says, "Hey, this is a Python package, and you can treat it as such, specifically for import statements."
Let's look at our search page.
Here, we have the stub for our search page object class.
""" This module contains DuckDuckGoSearchPage, the page object for the DuckDuckGo search page. """ class DuckDuckGoSearchPage: def __init__(self, browser): self.browser = browser def load(self): # TODO pass def search(self, phrase): # TODO Pass
I have some comments at the top just to denote what it is,
And inside of here, I have our
__init__ method, which acts like the constructor. Notice how it takes in the
browser, which will be passed in from the test case.
I want to set my local
self.browser variable to be whatever
browser it's passed in. This is basically just an instance variable initialization.
Furthermore, I have two stubbed methods — one for loading the page and one for searching the page with a given textual phrase.
For now, I'm going to use Python's
pass command to just “no-op” these methods, and I'll put a “TODO” comment so I know to come back and check them later.
Let's take a look at our result page as well.
I've put it in a separate Python module named
result.py, but it looks very, very similar to the search page.
""" This module contains DuckDuckGoResultPage, the page object for the DuckDuckGo search result page. """ class DuckDuckGoResultPage: def __init__(self, browser): self.browser = browser def result_link_titles(self): # TODO return  def search_input_value(self): # TODO return "" def title(self): # TODO return ""
Comments at the top, class name, the same
__init__ method for the WebDriver
The only difference here are the stubbed methods.
Notice here instead of no-op’ing, I'm returning something. That's because, as we said for our result page, the interaction methods don't necessarily do something, but rather they get something.
I don't want to fill in that quite yet because I just want to have a stub, so I'm going to put my “TODO” comments and I'm going to return appropriate values.
So, for result_link_titles, since this is supposed to represent a list of titles, I'll return an empty list.
And for the other two methods, since they're supposed to return String-based values, I'll just return empty Strings for now.
Now, that we have our page object stubs, we can actually implement the Python code in our test case.
test_search.py. Here, we can see the entire test case is implemented.
""" These tests cover DuckDuckGo searches. """ from pages.result import DuckDuckGoResultPage from pages.search import DuckDuckGoSearchPage def test_basic_duckduckgo_search(browser): search_page = DuckDuckGoSearchPage(browser) result_page = DuckDuckGoResultPage(browser) PHRASE = "panda" # Given the DuckDuckGo home page is displayed search_page.load() # When the user searches for "panda" search_page.search(PHRASE) # Then the search result title contains "panda" assert PHRASE in result_page.title() # And the search result query is "panda" assert PHRASE == result_page.search_input_value() # And the search result links pertain to "panda" titles = result_page.result_link_titles() matches = [t for t in titles if PHRASE.lower() in t.lower()] assert len(matches) > 0 # TODO: Remove this exception once the test is complete raise Exception("Incomplete Test")
At the top, I have my comments. Then I have my
Notice how I'm referring to the pages package
from pages.result and
from pages.search to import the different page object classes that I need in my test case function.
We will take the
browser that we get from the fixture. That's the WebDriver object as you recall, and I will use it to construct my 2 different search pages.
Then each of my steps is nothing more than page object calls.
Given the DuckDuckGo home page is displayed — that's
When the user searches for a phrase —that's
search_page.search with the “PHRASE” that I want.
Then the search result title contains my phrase —
assert that your “PHRASE” is in the
And the search result query is "panda" —
assert that your “PHRASE” equals the
Finally, for title and result page,
result_link_titles, doing a loop over everything —
assert that your lowercase version of your phrase is in the lowercase version of the title.
We have completely implemented all of our steps using page object calls and maybe a few other little Python tricks.
That's pretty nice because from now on, the only things left to implement are the page object calls themselves with Selenium WebDriver.
Finally, just note that even though I've finished the code of the test case function, the test case is not fully implemented yet, so I'm going to leave in my exception for now.