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



All Articles