Page Objects can be used as a powerful method of abstraction (isolation) of your tests from technical implementation. It is important to remember that they (Page Objects) can be used to increase the stability of tests and maintain the principle of
DRY (do not repeat yourself) - by encapsulating the functionality (website) in simple methods.
In other words
Page Object is an instance of a class that abstracts (isolates) the user interface from the test environment, presents methods for interacting with the user interface, and extracts the necessary information.
Terminology
The term
Page Object is too general a concept. In my experience,
Page Object includes the following 3 types:
- Component Objects represents specific components or widgets in the user interface. For example: tables, menus, articles and other blocks containing a group of components.
- Page Object describes a specific area or user interface in a web application. It may consist of several Component Objects and may contain convenient methods for interacting with the abstraction that is contained within this object.
- Experience is used to group complex functionality, testing of which requires several steps, or interaction with several pages. From my own experience, I used this concept to abstract complex behavior on a page (testing educational pages, creating a new user, etc.)
Examples
Consider a simple
RSpec Capybara test that creates blogs and does not use page objects:
require 'feature_helper' feature 'Blog management', type: :feature do scenario 'Successfully creating a new blog' do visit '/' click_on 'Form Examples' expect(page).to have_content('Create Blog') fill_in 'blog_title', with: 'My Blog Title' fill_in 'blog_text', with: 'My new blog text' click_on 'Save Blog' expect(page).to have_selector('.blog--show') expect(page).to have_content('My Blog Title') expect(page).to have_content('My new blog text') end scenario 'Entering no data' do visit '/' click_on 'Form Examples' expect(page).to have_content('Create Blog') click_on 'Save Blog' expect(page).to have_content('4 errors stopped this form being submitted') expect(page).to have_content("Title can't be blank") expect(page).to have_content("Text can't be blank") expect(page).to have_content('Title is too short') expect(page).to have_content('Text is too short') end end
Let's take a closer look at the code; it has several problems. There are the following actions: switching to the corresponding page, interacting with the page and checking the content. Part of the code is duplicated, but this can be fixed by sticking to the
DRY principle.
It is important to understand that this code is difficult to maintain if there are changes in the application under test. For example, element classes, names and identifiers can change, which requires regular updates of the test code.
Also in this code there is no 'Semantic context', it is difficult to understand which lines of code are logically grouped.
Introduction to Page Objects
As discussed in the terminology section,
Page Objects can be used to represent presentation-level abstractions.
Taking the previous example and using
Page Object to create new blogs and view blogs, we can clear the code of the previous example.
Having got rid of specific information about technical implementation, the final result (code) should be readable and should not contain specific information about the user interface (id, css classes, etc.).
require 'feature_helper' require_relative '../pages/new_blog' require_relative '../pages/view_blog' feature 'Blog management', type: :feature do let(:new_blog_page) { ::Pages::NewBlog.new } let(:view_blog_page) { ::Pages::ViewBlog.new } before :each do new_blog_page.visit_location end scenario 'Successfully creating a new blog' do new_blog_page.create title: 'My Blog Title', text: 'My new blog text' expect(view_blog_page).to have_loaded expect(view_blog_page).to have_blog title: 'My Blog Title', text: 'My new blog text' end scenario 'Entering no data' do new_blog_page.create title: '', text: '' expect(view_blog_page).to_not have_loaded expect(new_blog_page).to have_errors "Title can't be blank", "Text can't be blank", "Title is too short", "Text is too short" end end
Creating Page Objects
The first step in creating
Page Objects is to create a
basic page class structure:
module Pages class NewBlog include RSpec::Matchers include Capybara::DSL
Connecting (enabling)
Capybara :: DSL to allow
Page Objects instances to use the methods available in
Capybara
has_css? '.foo' has_content? 'hello world' find('.foo').click
In addition, I used
include RSpec :: Matchers
in the above examples to use the
expectation RSpec library.
Do not violate the agreement,
Page Objects should not include
expect (expectations) . However, where appropriate, I prefer this approach to rely on
Capybara's built-in mechanisms for handling conditions.
For example, the following
Capybara code will
expect the presence of
'foo' inside
Page Objects (in this case,
self ):
expect(self).to have_content 'foo'
However, in the following code:
expect(page_object.content).to match 'foo'
Unforeseen errors are possible (a floating test may occur), since
page_object.content is immediately checked for compliance with the condition, and possibly has not yet been declared. For more examples, I would recommend reading
thoughtbot's writing reliable asynchronous integration tests with Capybara .
Method Creation
We can abstract (describe) the place (region) from which we want to obtain data, in the framework of one method:
def visit_location visit '/blogs/new'
It's important to choose the
semantically correct names for the methods for your
Page Objects
def create(title:, text:)
In general, it is important to follow the principle of functionally integrated methods and, where possible, adhere to the principle of single responsibility (Single Responsibility Principle).
Component objects
In our example, we use the NewBlog class, but there is no implementation to create.
Since we interact with the form, we could additionally introduce a class to represent this component:
Where methods implementation for BlogForm can be hidden:
module Components class BlogForm include RSpec::Matchers include Capybara::DSL def create(title:, text:) within blog_form do fill_in 'blog_title', with: title fill_in 'blog_text', with: text click_on 'Save Blog' end end private def blog_form find('.blog--new') end end end
Together
Using the above classes, you can now query and create instances of the
Page Objects of your page as part of the description of the object.
require 'feature_helper' require_relative '../pages/new_blog' require_relative '../pages/view_blog' feature 'Blog management', type: :feature do let(:new_blog_page) { ::Pages::NewBlog.new } let(:view_blog_page) { ::Pages::ViewBlog.new }
Note: I intentionally created the page object manually at the top of the object file. In some RSpec tests, it may be convenient to automatically download all support files and provide access to them in object files, however, this can lead to excessive workloads when using large pieces of code. In particular, this will lead to slow startup and potential unintended cyclic dependencies.
Call Page Objects
Now in each scenario we will have access to instances of
new_blog_page and
view_blog_page :
scenario 'Successfully creating a new blog' do new_blog_page.create title: 'My Blog Title', text: 'My new blog text' expect(view_blog_page).to have_loaded expect(view_blog_page).to have_blog title: 'My Blog Title', text: 'My new blog text' end
Naming Conventions / Predicate Methods
As with most things in Rails / Ruby, there are conventions that may seem insignificant (not binding) completely at a glance.
In our tests, we interacted with the page object using
have_loaded and
have_blog :
expect(view_blog_page).to have_loaded expect(view_blog_page).to have_blog title: 'My Blog Title', text: 'My new blog text'
However, the method names of our page object actually
has_loaded? and
has_blog? :
def has_loaded?
This is a subtle distinction that needs attention. For more information on this convention, I would recommend reading the following
predicate matchers link.
Git, the source code used in the examples
Original