Page Objects могут быть использованы как мощный метод абстракции (изоляции) ваших тестов от технической реализации. Важно помнить, их (Page Objects) можно использовать для увеличения стабильности тестов и поддержания принципа DRY (do not repeat yourself) — посредством инкапсуляции функционала (вебсайта) в простых методах.
Другими словами
Page Object — это экземпляр класса, который абстрагирует (изолирует) пользовательский интерфейс от тестовой среды, представляет методы для взаимодействия с пользовательским интерфейсом и извлекает необходимую информацию.
Терминология
Термин Page Object слишком обобщенное понятие. По моему опыту Page Object включает в себя следующие 3 типа:
- Component Objects представляет определенные компоненты или виджеты в пользовательском интерфейсе. Например: таблицы, меню, статьи и прочие блоки содержащие в себе группу компонентов.
- Page Object описывает определенную область, или пользовательский интерфейс в веб приложении. Он может состоять из нескольких Component Objects и может содержать удобные методы для взаимодействия с абстракцией, которая содержится внутри данного объекта.
- Experience используется для группировки сложного функционала, тестирование которого требует выполнения нескольких шагов, или взаимодействия с несколькими страницами. По своему опыту я использовал эту концепцию для абстракции сложного поведения на странице (тестирование обучающих страниц, создание нового пользователя и т.д.)
Примеры
Рассмотрим простой тест RSpec Capybara, который создает блоги и не использует объекты страницы:
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
Рассмотрим код внимательнее, в нем есть несколько проблем. Здесь есть следующие действия: переход на соответствующую страницу, взаимодействие со страницей и проверка контента. Часть кода дублируется, но это можно исправить придерживаясь принципа DRY.
Важно понимать, что этот код трудно поддерживать, если есть изменения в тестируемом приложении. Например, классы элементов, имена и идентификаторы могут изменяться, что требует, регулярного потребует обновления кода теста.
Так же в этом коде нет 'Семантического контекста', трудно понять, какие строки кода логически сгруппированы.
Введение в Page Objects
Как обсуждалось в разделе терминологии, Page Objects могут использоваться для абстракций уровня представления.
Взяв предыдущий пример и применив Page Object для создания новых блогов и просмотра блогов, мы можем очистить код предыдущего примера.
Избавившись от конкретных сведений о технической реализации конечный результат (код) должны быть читабельным и не должен содержать конкретных сведений о пользовательском интерфейсе (id, css классы и т.д.).
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
Создание Page Objects
Первый шаг создания Page Objects это создание структуры basic page class:
module Pages
class NewBlog
include RSpec::Matchers
include Capybara::DSL
# ...
end
end
Подключение (включение) Capybara :: DSL позволить экземплярам Page Objects использовать методы доступные в Capybara
has_css? '.foo'
has_content? 'hello world'
find('.foo').click
Кроме того, я использовал
include RSpec :: Matchers
в приведенных выше примерах, чтобы использовать RSpec библиотеку expectation.
Не стоит нарушать соглашения, Page Objects не должен включать в себя expect (ожидания). Однако где уместно я предпочитаю этот подход, чтобы полагаться на встроенные механизмы Capybara для обработки условий.
Например, следующий код Capybara будет expect (ожидать), наличия 'foo' внутри Page Objects (в данном случае это self):
expect(self).to have_content 'foo'
Тем не менее, в следующем коде:
expect(page_object.content).to match 'foo'
Возможны непредвиденные ошибки (возможно возникновение плавающего теста), так как page_object.content сразу проверяется на соответствие условию, и возможно, еще не объявлен. Для большего количества примеров я бы порекомендовал прочитать thoughtbot's написание надежных асинхронных интеграционных тестов с Capybara.
Создание методов
Мы может абстрагировать (описать) место (область), из которой мы хотим получить данных, в рамках одного метода:
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
Важно выбрать семантически верные имена для методов для ваших Page Objects
def create(title:, text:)
# ...
end
def has_errors?(*errors)
# ...
end
def has_error?(error)
# ...
end
В целом, важно следовать принципу функционально объединенных методов и, где возможно, придерживаться принципа единой ответственности (Single Responsibility Principle).
Component Objects
В нашем примере мы используем класс NewBlog, но реализация для создания отсутствует.
Поскольку мы взаимодействуем с формой, мы могли бы дополнительно ввести класс для представления этого компонента:
# ...
def create(title:, text:)
blog_form.new.create title: title,
text: text
end
# ...
private
def blog_form
::Components::BlogForm
end
Где может быть спрятана реализация методов для BlogForm:
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
Все вместе
С помощью приведенных выше классов теперь можно запрашивать и создавать экземпляры объектов (Page Objects) вашей страницы в рамках описания объекта.
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
Примечание: Я намеренно создал объект страницы вручную в верхней части файла объектов. В некоторых RSpec тестах может быть удобно автоматически загружать все файлы поддержки и предоставлять доступ к ним в файлах объектов, однако это может привести к чрезмерным нагрузкам при использовании больших кусков кода. В частности, это приведет к медленному запуску и потенциальным непреднамеренным циклическим зависимостям.
Вызов Page Objects
Теперь в каждом сценарии у нас будет доступ к экземплярам new_blog_page и 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
Как и в большинстве вещей в Rails / Ruby, существуют соглашения, которые могут показаться незначительными (не обязательными к исполнению) полностью с первого взгляда.
В наших тестах мы взаимодействовали с объектом страницы с помощью have_loaded и 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'
Тем не менее, имена методов нашего объекта страницы на самом деле has_loaded? и has_blog?:
def has_loaded?
# ...
end
def has_blog?(title:, text:)
# ...
end
Это тонкое различие, на которое нужно обратить внимание. Для получения более подробной информации об этом соглашении я бы рекомендовал к прочтению следующую ссылку predicate matchers.
Git, исходный код используемый в примерах
Оригинал
Автор: hihilisk