Тестирование пользовательского функционала вебсайта с помощью Capybara page objects

в 15:08, , рубрики: capybara, page object, rspec, tdd, Тестирование веб-сервисов, тесты

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js