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 tests
directory.
In theory, 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,
Then, class DuckDuckGoSearchPage
.
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 browser
object.
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.
Let's open 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 import
statements.
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 search_page.load
.
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 result_page.title
.
And
the search result query is "panda" — assert
that your “PHRASE” equals the result_page.search_input_value
.
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.