Testing website user functionality with Capybara page objects

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:


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 # ... end end 

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 can be beneficial to assert something positive about the page # before progressing with your tests at this point # # This can be useful to ensures that the page has loaded successfully, and any # asynchronous JavaScript has been loaded and retrieved etc. # # This is required to avoid potential race conditions. expect(self).to have_loaded end def has_loaded? self.has_selector? 'h1', text: 'Create Blog' end 

It's important to choose the semantically correct names for the methods for your Page Objects

 def create(title:, text:) # ... end def has_errors?(*errors) # ... end def has_error?(error) # ... end 

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:

 # ... def create(title:, text:) blog_form.new.create title: title, text: text end # ... private def blog_form ::Components::BlogForm end 

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 } # ... end 

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? # ... end def has_blog?(title:, text:) # ... end 

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

Source: https://habr.com/ru/post/466527/


All Articles