- Час: 30-40 min
- Рівень: Середній/Високий
- Код: GitHub
В цій статті ми на простому прикладі розглянемо як можна покращитиRails 5.1.3 System Test
використовуючи Plain Old Ruby Objects,collaborators, delegators і module.
Крок #0
Приклад до рефакторингу
# test/system/users_test.rb
require "application_system_test_case"
class UsersTest < ApplicationSystemTestCase
test "visiting the index" do
visit users_url
assert_selector "h1", text: "User"
end
test 'creating new user' do
visit users_url
click_on 'New User'
fill_in 'First name', with: 'Bill'
fill_in 'Last name', with: 'Bird'
click_on 'Create User'
visit users_url
assert_text 'Bill Bird'
end
test 'editing existing user' do
User.new(first_name: 'Bill', last_name: 'Bird').save
visit edit_user_url(User.first)
fill_in 'First name', with: 'First'
fill_in 'Last name', with: 'Last'
click_on 'Update User'
assert_text 'First Last'
end
end
Тест перевіряє три речі:
- Чи можливо відкрити сторінку зі списком користувачів і чи має вона очікувану структуру
- Чи можливо додати нового користувача і чи буде новий користувач на сторінці зі списком користувачів
- Чи можливо оновити інформацію про користувача і чи будуть відображені зімни на сторінці зі списком користувачів
Крок #1
В цьому кроці ми:
- Створимо новий абстрактний клас який в майбутному допоможе нам описати структуру та функціонал HTML сторінок
- Створимо page class для тестування сторінки з інформацією про користувача
- Використаємо новий page class в тесті
Для початку ми додамо абстрактний клас, який має один метод для визначення елементів на сторінці, зміни можна переглянути у відповідному комміті
# test/support/pages/base.rb
module Pages
class Base
Error = Class.new(StandardError)
attr_reader :current_session
attr_reader :url
def self.has_node(method_name, selector, default_selector = :css, options = {})
case default_selector
when :css
define_method(method_name) do
css_selector = @css_wrapper + ' ' + selector
current_session.first(default_selector, css_selector.strip, options)
end
when :xpath
# XPATH accessor
define_method(method_name) do
current_session.first(default_selector, selector, options)
end
else
fail Error, "Unknown selector #{default_selector}"
end
end
private
# initialize with Capybara session
def initialize(url:, css_wrapper: ' ', current_session: Capybara.current_session)
@current_session = current_session
@url = url
@css_wrapper = css_wrapper
end
end
end
Давайте детальніше розглянемо метод initilaize
та instance variables у ньому:
-
@current_session
- за замовчуваннямCapybara.current_session
,об’єкт-collaboratior що дозволяє нам використовувати driver всередині методуhas_node
-
@url
- обов’язкова змінна, URL сторінки що тестується -
@css_wrapper
- за замовчуванням порожня стрічка, допоміжний параметр, використовується коли всі елементи на сторінці знаходяться всередині елементу з певним CSS класом
Тепер додамо новий клас що описує сторінку з інформацією про користувача
# test/support/pages/users/show.rb
require_relative '../base'
module Pages
module Users
class Show < Pages::Base
has_node :notice, '#notice'
has_node :edit_user_link, 'a', :css, text: 'Edit'
has_node :back_link, '//a[text()="Back"]', :xpath
end
end
end
Є три способи для визначення елементу на сторінці:
- За CSS id
- За типом і текстом всередині елементу
- За xpath
Варто запам’ятати:
-
has_node
лише обгортка навколоCapybara::Node::Finders#first тому є різні способи отримати один і той же результат -
has_node
повертає такий же результат що й Capybara::Node::Finders#first, якщо елемент був знайдений то це об’єкт Capybara::Node::Element
Тепер використаємо Pages::Users::Show
в тесті для UsersController#show
test 'creating new user' do
visit users_url
click_on 'New User'
fill_in 'First name', with: 'Bill'
fill_in 'Last name', with: 'Bird'
click_on 'Create User'
page = ::Pages::Users::Show.new(url: user_path(User.last))
assert page.notice.text == 'User was successfully created.'
assert page.edit_user_link.text == 'Edit'
assert page.back_link.text == 'Back'
visit users_url
assert_text 'Bill Bird'
end
цей крок досить малий, лише для того щоб зрозуміти як використовувати page classes
Крок #2
В цьому кроці ми:
- Додамо новий
Pages::Base#visit
метод - Додамо
Rails.application.routes.url_helpers
доPages::Base
для того щоб мати доступ до routes - Додамо
Pages::Users::New
,Pages::Users::Edit
,Pages::Users::Index
- Використаємо нові класи для рефакторингу
Я не додаватиму код нових класів тут, його можна знайти у відповідному комміті. Натомість давайте поглянемо на тест, що їх використовує:
# test/system/users_test.rb
require 'application_system_test_case'
require File.join(Rails.root.to_s, 'test', 'support', 'pages', 'users', 'show')
require File.join(Rails.root.to_s, 'test', 'support', 'pages', 'users', 'new')
require File.join(Rails.root.to_s, 'test', 'support', 'pages', 'users', 'index')
require File.join(Rails.root.to_s, 'test', 'support', 'pages', 'users', 'edit')
class UsersTest < ApplicationSystemTestCase
test "visiting the index" do
visit users_url
assert_selector "h1", text: "User"
end
test 'creating new user' do
::Pages::Users::Index.new.instance_eval do
visit
new_user_link.click
end
::Pages::Users::New.new.instance_eval do
visit
first_name.set( 'Bill' )
last_name.set( 'Bird' )
create_user_button.click
end
page = ::Pages::Users::Show.new(url: user_path(User.last))
assert page.notice.text == 'User was successfully created.'
assert page.edit_user_link.text == 'Edit'
assert page.back_link.text == 'Back'
::Pages::Users::Index.new.visit
assert_text 'Bill Bird'
end
test 'editing existing user' do
User.new(first_name: 'Bill', last_name: 'Bird').save
::Pages::Users::Edit.new(url: edit_user_url(User.first)).instance_eval do
visit
first_name.set( 'First' )
last_name.set( 'Last' )
update_user_button.click
end
::Pages::Users::Index.new.visit
assert_text 'First Last'
end
end
У нас лишилось ще три кроки попереду проте давайте підсумуємо що ми вже отримали:
- Ми використовуємо методи класу а не CSS/XPATH отож якщо структура сторінки зміниться ми повинні будемо змінити лише клас щоб виправити тести
- Завдяки використанню collaborator objects код згрупований всередині блоків, його простіше зрозуміти і одразу очевидно на якій сторінці виконується кожна лінія коду
Крок #3
В цьому кроці ми:
- Додамо можливість перевіряти чи присутній елемент всередині page classes
- Додамо у
Pages::Users::Show
метод для перевірки структури сторінки
Для початку розглянемо зміни в тесті ( всі зміни у відповідному комміті)
До
# test/system/users_test.rb
test 'creating new user' do
# Not important piece
page = ::Pages::Users::Show.new(url: user_path(User.last))
assert page.notice.text == 'User was successfully created.'
assert page.edit_user_link.text == 'Edit'
assert page.back_link.text == 'Back'
::Pages::Users::Index.new.visit
assert_text 'Bill Bird'
end
Після
# test/system/users_test.rb
test 'creating new user' do
# Not important piece
::Pages::Users::Show.new(test: self, url: user_path(User.last)).instance_eval do
check_main_elements_presence
assert notice.text == 'User was successfully created.'
end
::Pages::Users::Index.new.visit
assert_text 'Bill Bird'
end
Метод Pages::Users::Show#check_main_elements_presence
# test/support/pages/users/show.rb
def check_main_elements_presence
notice_present?
edit_user_link_present?
back_link_present?
end
Для отримання такого результату ми:
- Змінили
Pages::Base#initialize
- тепер він очікує новий об’єкт-collaboratortest:
- Змінили
Pages::Base#has_node
- тепер він додає метод для доступу до елементу та перевірки наявності елементу на сторінці -*_present?
Крок #4
В цьому кроці ми вилучимо спільний функціонал у модуль (відповідний комміт)
Для початку порівняємо Pages::User::Edit
та Pages::User::New
# pages/user/edit.rb
require_relative '../base'
module Pages
module Users
class Edit < Pages::Base
has_node :first_name, '#user_first_name'
has_node :last_name, '#user_last_name'
has_node :update_user_button, '//input[@value ="Update User"]', :xpath
end
end
end
# pages/user/new.rb
require_relative '../base'
module Pages
module Users
class New < Pages::Base
has_node :first_name, '#user_first_name'
has_node :last_name, '#user_last_name'
has_node :create_user_button, '//input[@value= "Create User"]', :xpath
private
def http_path
new_user_path
end
end
end
end
обидва мають однакові елементи first_name
та last_name
, що не дивно - ми render один і той самий partial form
на обох сторінках. Окрім того ми заповнюємо цю форму коли тестуємо ці сторінки. Давайте вилучимо спільний функціонал у модуль.
Pages::Users::Partials::UserForm
модуль
# test/support/pages/users/partials/user_form.rb
module Pages
module Users
module Partials
module UserForm
def self.included(clazz)
clazz.has_node :first_name, '#user_first_name'
clazz.has_node :last_name, '#user_last_name'
end
def fill_out_user_form(first: 'Bill', last: 'Bird')
first_name.set(first)
last_name.set(last)
end
end
end
end
end
Page classes після рефакторингу
# pages/user/edit.rb
require_relative '../base'
module Pages
module Users
class Edit < Pages::Base
include Partials::UserForm
has_node :update_user_button, '//input[@value ="Update User"]', :xpath
end
end
end
# pages/user/new.rb
require_relative '../base'
module Pages
module Users
class New < Pages::Base
include Partials::UserForm
has_node :create_user_button, '//input[@value= "Create User"]', :xpath
private
def http_path
new_user_path
end
end
end
end
Крок #5
В цьому кроці ми:
- Додамо можливість робити скріншот до page classes
- Порівняємо як виглядав тест до Крок #1 та після Крок #5
Перша частина досить проста, оскільки ми вже маємо тест як об’єкт-collaboratorу Pages::Base
нам лише потрібно додати take_screenshot
до списку методів які ми делегуємо,всі зміни можна переглянути у відповідному комміті
Тепер давайте порівняємо що ми мали на початку і як тест виглядає після рефакторингу
До
# test/system/users_test.rb
require "application_system_test_case"
class UsersTest < ApplicationSystemTestCase
test "visiting the index" do
visit users_url
assert_selector "h1", text: "User"
end
test 'creating new user' do
visit users_url
click_on 'New User'
fill_in 'First name', with: 'Bill'
fill_in 'Last name', with: 'Bird'
click_on 'Create User'
visit users_url
assert_text 'Bill Bird'
end
test 'editing existing user' do
User.new(first_name: 'Bill', last_name: 'Bird').save
visit edit_user_url(User.first)
fill_in 'First name', with: 'First'
fill_in 'Last name', with: 'Last'
click_on 'Update User'
assert_text 'First Last'
end
end
Після
# test/system/users_test.rb
require 'application_system_test_case'
require File.join(Rails.root.to_s, 'test', 'support', 'pages', 'users', 'show')
require File.join(Rails.root.to_s, 'test', 'support', 'pages', 'users', 'new')
require File.join(Rails.root.to_s, 'test', 'support', 'pages', 'users', 'index')
require File.join(Rails.root.to_s, 'test', 'support', 'pages', 'users', 'edit')
class UsersTest < ApplicationSystemTestCase
test "visiting the index" do
visit users_url
assert_selector "h1", text: "User"
end
test 'creating new user' do
::Pages::Users::Index.new(test: self).instance_eval do
visit
new_user_link.click
take_screenshot
end
::Pages::Users::New.new.instance_eval do
visit
fill_out_user_form
create_user_button.click
end
::Pages::Users::Show.new(test: self, url: user_path(User.last)).instance_eval do
check_main_elements_presence
assert notice.text == 'User was successfully created.'
end
::Pages::Users::Index.new.visit
assert_text 'Bill Bird'
end
test 'editing existing user' do
User.new(first_name: 'Bill', last_name: 'Bird').save
::Pages::Users::Edit.new(url: edit_user_url(User.first)).instance_eval do
visit
fill_out_user_form(first: 'First', last: 'Last')
update_user_button.click
end
::Pages::Users::Index.new(test: self).instance_eval do
visit
assert_text 'First Last'
end
end
end
версія ‘Після’ має певні переваги, ми перерахуємо їх у підсумку
Підсумок
Переваги OO підходу:
- Тести менш ‘крихкі’ - якщо структура чи логіка сторінки зміниться досить буде змінити лише page class
- Тести більш зрозумілі - завдяки використанню
instance_eval
та блоків завжди зрозуміло на якій сторінці ви знаходитесь - Значно простіше описати структуру сторінки
- Однаковий функціонал можна помістити в модуль
- Інші члени команди можуть використовувати готові page classes
- Pages classes є POROs, Ви можете використовувати всю красу/потужність Ruby в них
Код:
Для роздумів:
- Мені не подобається що
Pages::Base
маєinclude Rails.application.routes.url_helpers
. Це було зроблено лише щоб показати що статичний URL може бути частиною page class, має бути кращий спосіб -
has_node
працює лише з одним елементом, варто додатиhas_nodes
для колекцій - В залежності від використаного фреймворку, методи делеговані в
Pages::Base
відрізнятимуться, проте його можна використовувати з іншими фреймворками (RSpec, …) - Замість багатьох тестів можна мати один супер-тест, тоді не доведеться чистити базу даних, можна групувати частини тесту за роллю користувача. Додаткові дані в базі можуть допомогти знайти глюки або лише ускладнити Ваше життя =)
Top comments (0)