Better Testing with Page Object Design in Django
Overview
A Page Object Model is a design pattern that helps to write better functional tests in a Django project, by encapsulating web page elements in Page objects, and avoiding dealing with the browser driver in your tests at all.
Following this design, each webpage of your application would have a corresponding Page Object class that it is used to interact with from your tests scripts. A tipical operation would be to find an element and perform an operation on it, e.g.: find a text box and fill it with sendKeys.
Idea
When doing functional tests with tools like Selenium, you have an API to handle a browser and end up mixing a lot of code to locate UI elements in pages to be able to test some behaviour on them.
To have cleaner code, we can use Page Object models to represent each page of our app as an object thus helping us to:
- use DRY (Don’t repeat yourself) principle to minimize code
duplication
- Have all ID-locators in one place
- Set an interface between web page’s elements and tests.
- Avoid using WebDriver APIs in test methods
- Encapsulate the services of web pages, not only exposing their elements.
With the Page Object model approach, we focus in setting a wrapper of web pages’ actions, and focus in writing tests instead of always deal with HTML elements.
The elements and actions we should expose in a Page object should be restricted only to useful elements to a test, they are not meant to be a wrapper of all the Webpages’ elements like headers or footers.
Accessors
Page Object should encapsulate web pages elements like check-boxes, text-boxes, buttons, and links, by having accessor methods (getters and setters).
It acts as a Repository of web User Interface elements relevant to your app.
The rule of thumb is to model the structure in the page that makes sense to the user of the application.
Actions
When we are writing functional tests we need to simulate many actions that would be repeated in different tests.
Base Page
We can also have a Base Page Object class BasePage
to expose
actions used in all pages like
- getting page title
- looking for elements by name and ids
- visit that page, etc.
Then all other pages would inherit these attributes and methods.
Using
To navigate between pages, a Page Object should return another Page Object representing the other web page.
page object operations should return fundamental types (strings, dates) or other page objects.
Assertions
Where to put assertions? Assertions may be located inside Page Objects or in tests.
Assertions in page objects helps minimize duplication of assertions in tests.
On the other hand, Page Objects without assertions helps to have a clear separation of concerns, Page Objects shouldn’t be responsible of making assertions, only provide access to web page elements.
Example
For example to test a login page that it is using allauth package,
we can have pagemodelobjects/page.py
:
class BasePage(object):
url = None
def __init__(self, driver, live_server_url):
self.driver = driver
self.live_server_url = live_server_url
def fill_form_by_css(self, form_css, value):
elem = self.driver.find(form_css)
elem.send_keys(value)
def fill_form_by_id(self, form_element_id, value):
elem = self.driver.find_element_by_id(form_element_id)
elem.send_keys(value)
def fill_form_by_name(self, name, value):
elem = self.driver.find_element_by_name(name)
elem.send_keys(value)
@property
def title(self):
return self.driver.title
def navigate(self):
self.driver.get(
"{}{}".format(
self.live_server_url,
self.url
)
)
Then for the Login page we would have:
class LoginPage(BasePage):
USERNAME_NAME = "login"
PASSWORD_NAME = "password"
FORM_SUBMIT_TEXT = 'Sign In'
url = "/accounts/login/"
ERRORS_CLASS = 'errorlist'
def set_username(self, username):
self.fill_form_by_name(self.USERNAME_NAME, username)
def set_password(self, password):
self.fill_form_by_name(self.PASSWORD_NAME, password)
def getSignupForm(self):
return SignupPage(self.driver, self.live_server_url)
def get_errors(self):
return self.driver.find_element_by_class_name(self.ERRORS_CLASS)
def submit(self):
btn = self.driver.find_element(By.XPATH, '//button[text()="{}"]'.format(self.FORM_SUBMIT_TEXT))
btn.click()
return ProfilePage(self.driver, self.live_server_url)
def submitExpectingFailure(self):
btn = self.driver.find_element(By.XPATH, '//button[text()="{}"]'.format(self.FORM_SUBMIT_TEXT))
btn.click()
return LoginPage(self.driver, self.live_server_url)
class ProfilePage(BasePage):
pass
class Homepage(BasePage):
url = "/"
def getLoginForm(self):
return LoginPage(self.driver, self.live_server_url)
And using them from a test tests/test_login.py
:
from django.test.utils import override_settings
from tests.functional_tests.base import FunctionalTest
from pagemodelobjects.pages import Homepage
from users.factories import CustomUserFactory
@override_settings(
ACCOUNT_EMAIL_VERIFICATION='none',
)
class LoginTests(FunctionalTest):
"""
Loads the index webpage only once and use it through all testcases
"""
def setUp(self):
pass
def test_login_common_user(self):
# creates an user to log in
password = "hello"
user = CustomUserFactory.create(password=password)
homepage = Homepage(self.browser, self.live_server_url)
homepage.navigate()
login_page = homepage.getLoginForm()
login_page.navigate()
login_page.set_username(user.username)
login_page.set_password(password)
profile_page = login_page.submit()
self.assertIn(user.username, profile_page.title)
def test_login_denies_access(self):
homepage = Homepage(self.browser, self.live_server_url)
homepage.navigate()
login_page = homepage.getLoginForm()
login_page.navigate()
login_page.set_username("aaa")
login_page.set_password("foo")
login_page = login_page.submitExpectingFailure()
self.assertIn("not correct", login_page.get_errors().text)
With a place where we set up the browser driver
tests/functional_tests/base.py
:
from django.contrib.staticfiles.testing import StaticLiveServerTestCase, LiveServerTestCase
from selenium.webdriver.firefox.webdriver import WebDriver
class FunctionalTest(StaticLiveServerTestCase):
"""
Launches a live Django server in the background on setup, and
shuts it down on teardown. This allows the use of automated test
clients other than the Django dummy client such as, for example,
the Selenium client, to execute a series of functional tests
inside a browser and simulate a real user’s actions.
We’ll use the StaticLiveServerTestCase subclass with serves static
files during the execution of tests similar to what we get at
development time with DEBUG=True, i.e. without having to collect
them using collectstatic.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.browser = WebDriver()
cls.browser.implicitly_wait(10)
@classmethod
def tearDownClass(cls):
cls.browser.quit()
super().tearDownClass()
Just adjust CustomUserFactory
to yours and you can have it a try.
I have found this approach improved my tests by making them more readable and reusable.
References
- https://martinfowler.com/bliki/PageObject.html
- https://www.seleniumhq.org/docs/06_test_design_considerations.jsp#page-object-design-pattern
- https://selenium-python.readthedocs.io/locating-elements.html#locating-by-id
- https://github.com/SeleniumHQ/selenium/wiki/PageObjects
- https://github.com/SeleniumHQ/selenium/wiki/PageFactory
- https://docs.djangoproject.com/en/2.2/topics/testing/tools/
- August 1, 2023
- How to create a reusable Django app and distribute it with PIP or publish to pypi.orgJune 29, 2021
- How To Serve Multiple Django Applications with uWSGI and Nginx in Ubuntu 20.04October 26, 2020
- How to add favicon to Django in 4 stepsSeptember 3, 2020
- Categories in Django with BreadcrumbsAugust 30, 2020
- How To Migrate From SQLite To PostgreSQL In Django In 3 stepsAugust 28, 2020
- Practical guide to internationalize a Django app in 5 steps.August 24, 2020
- Disable new users singup when using Django's allauth packageSeptember 3, 2019
- How to add ads.txt to Django as requested by Google AdsenseAugust 30, 2019
- Have multiple submit buttons for the same Django formJuly 2, 2019
- Better Testing with Page Object Design in Django
- Generating slugs automatically in Django without packages - Two easy and solid approachesFebruary 14, 2019
- How to set up Django tests to use a free PostgreSQL database in HerokuFebruary 13, 2019
- Dynamically adding forms to a Django FormSet with an add button using jQueryFebruary 6, 2019
- Use of Django's static templatetag in css file to set a background imageFebruary 1, 2019
- Activate Django's manage.py commands completion in Bash in 2 stepsJanuary 29, 2019
- Sending Emails with Django using SendGrid in 3 easy stepsJanuary 9, 2019
- Adding Users to Your Django Project With A Custom User ModelSeptember 21, 2018
- Setting Up A Factory For One To Many Relationships In FactoryboyApril 17, 2018
- Generate UML class diagrams from django modelsMarch 24, 2018
- Set Up Ubuntu To Serve A Django Website Step By StepJuly 3, 2017
- Django Project Directory StructureJuly 16, 2016
- How to Have Different Django Settings for Development and Production, and environment isolationJune 10, 2016
- Django OverviewJune 2, 2016
Django Forms
- Adding a Cancel button in Django class-based views, editing views and formsJuly 15, 2019
- Using Django Model Primary Key in Custom Forms THE RIGHT WAYJuly 13, 2019
- Django formset handling with class based views, custom errors and validationJuly 4, 2019
- How To Use Bootstrap 4 In Django FormsMay 25, 2018
- Understanding Django FormsApril 30, 2018
- How To Create A Form In DjangoJuly 29, 2016
Articles
Subcategories
Except as otherwise noted, the content of this page is licensed under CC BY-NC-ND 4.0 . Terms and Policy.
Powered by SimpleIT Hugo Theme
·