Original:http://mikepackdev.com/blog_posts/41-component-based-acceptance-testing

Компонентне тестування прийняття

Ви чули про об’єкти сторінки ? Вони приголомшливі. Я буду називати їх як ОЗ. Вони були задумані як набір вказівок щодо організації дій, які користувач здійснює в рамках програми, і вони працюють досить добре. Однак у ОП є кілька недоліків. А саме, вказівки (або їх відсутність) щодо того, як обробляти фрагменти програми, які поділяються на сторінках. Ось де компоненти корисні.

Компонент - це частина сторінки; повна сторінка складається з нуля або більше компонентів. Поряд із компонентами сторінка також може мати унікальні сегменти, які не вписуються добре в компонент.

У сучасній павутині компоненти - це більше, ніж візуальна абстракція. Веб-компоненти зростають у використанні, оскільки рамки, такі як Angular , Ember та React, виступають за їх прийняття, щоб правильно інкапсулювати HTML, CSS та JavaScript. Якщо ви вже впорядковуєте свій фронтальний код за компонентами, ця стаття буде відчувати себе природно. Не випадково поведінкове інкапсуляція компонентів в рамках приймальних тестів часто є тим самим поведінковим інкапсуляцією компонентів у передньому коді. Але я трохи попереду себе ...

Давайте швидко переглянемо PO. ОП датуються 2004 роком , коли спочатку називали WindowDrivers . Selenium WebDriver популяризував методику під назвою « Об’єкти сторінки» . Мартін Фаулер писав про свій останній підхід до виробничих організацій у 2013 році. Існує навіть цікаве академічне дослідження щодо впливу організацій, що займаються виробництвом . Взагалі кажучи, один PO представляє одну сторінку, що тестується. Він знає деталі взаємодії з цією сторінкою, наприклад, як знайти елемент для натискання.

Тести приймання мають дві основні категорії подій: дії та твердження. Дії - це взаємодія з браузером. Твердження - це перевірка того, що браузер у правильному стані. Співтовариство вважає за краще, щоб ОП виконували дії на сторінці, але не стверджували. Твердження повинні міститись у самому тесті.

Щоб продемонструвати PO-адреси та компоненти, давайте напишемо кілька тестів на прийняття приблизно декількох основних взаємодій зі сторінкою профілю Twitter, зображеною нижче.

Twitter Profile Page

При натисканні на значок блакитного пір’я праворуч вгорі відкриється діалогове вікно, що дозволяє користувачеві скласти твіт.

Twitter Compose Dialog

Для цієї демонстрації ми будемо використовувати Ruby, RSpec та Capybara, щоб імітувати ці взаємодії в наших тестах прийняття, але правила, про які ми розповімо тут, можна легко перекласти на інші набори інструментів.

Ми можемо почати з PO, який виглядає наступним чином. Цей простий PO вміє відвідувати сторінку профілю, переходити до підписників користувача та починати складати твіт.

module Page
 
class Profile
    include
Capybara::DSL

   
def navigate(handle)
      visit
"/#{handle}"
   
end

   
def navigate_to_followers
      click_link
'Followers'
   
end

   
def open_tweetbox
      click_button
'Tweet'
   
end
 
end
end

Наступний тест використовує кожну частину вищезгаданого PO.

describe 'the profile page' do
  let
(:profile_page) { Page::Profile.new }
 
  before
do
    profile_page
.navigate('mikepack_')
 
end
 
  it
'allows me to navigate to the followers page' do
    profile_page
.navigate_to_followers
 
    expect
(current_path).to eq('/mikepack_/followers')
 
end
 
  it
'allows me to write a new tweet' do
    profile_page
.open_tweetbox
 
    expect
(page).to have_content('Compose new Tweet')
 
end
end

Це майже все, що робить PO. Для мене на даний момент є кілька невирішених питань, але ми значною мірою продемонстрували цю закономірність. Для того, щоб виділити, де починаються зриви PO, давайте моделювати сторінку "підписників" за допомогою PO.

module Page
 
class Followers
    include
Capybara::DSL

   
def navigate(handle)
      visit
"/#{handle}/followers"
   
end
 
   
def navigate_to_tweets
      click_link
'Tweets'
   
end
 
   
# Duplicated from Page::Profile
   
def open_tweetbox
      click_button
'Tweet'
   
end
 
end
end

Ну, ми зіткнулися з нашою першою проблемою: користувач може створити твіт як з основної сторінки профілю, так і зі сторінки підписників. Нам потрібно поділитися дією #open_tweetbox між цими двома сторінками. Загальноприйнята мудрість полягає в тому, щоб створити ще одну "сторінку твіттера", як-от наступну. Ми перемістимо метод #open_tweetbox у новий PO та з інших PO, і перейменоваємо його у #open .

module Page
 
class Tweetbox
    include
Capybara::DSL
 
   
def open
      click_button
'Tweet'
   
end
 
end
end

Наш тест на сторінці профілю тепер включає в себе новий поштовий скринька PO, і наш код набагато більше ДУХОГО.

describe 'the profile page' do
  let
(:profile_page) { Page::Profile.new }
  let
(:tweetbox_page) { Page::Tweetbox.new } # New code
 
  before
do
   
# Original setup remains the same
 
end
 
  it
'allows me to navigate to the followers page' do
   
# Original test remains the same
 
end
 
  it
'allows me to write a new tweet' do
    tweetbox
.open
 
    expect
(page).to have_content('Compose new Tweet')
 
end
end

Зараз ми вирішили ще одну загадку: якщо на сторінці твітів та на сторінках підписників є можливість скласти новий твіт, чи ми дублюємо тест на складання твіту на обох сторінках? Чи розміщуємо ми її на одній сторінці, а не на іншій? Як ми обираємо, яку сторінку?

Саме тут на сцену виходять компоненти. Насправді у нас вже майже є компонент: Сторінка :: Tweetbox . Мені не подобається загальноприйнята мудрість робити будь-яку частину сторінки іншою PO, як це було зроблено зі сторінкою :: Tweetbox . На мою думку, ОП повинні представляти повні сторінки. Я вважаю, що цілі сторінки та частини сторінок (тобто компоненти) несуть суттєво різну семантику. Ми повинні по-різному ставитися до виробничих та компонентів, хоча їх реалізація здебільшого послідовна. Поговоримо про відмінності.

Ось мої вказівки щодо сторінок та компонентів:

  1. Якщо вона поділяється на сторінках, це компонент.
  2. Сторінки мають URL-адреси, а компоненти - ні.
  3. Сторінки мають твердження, компоненти - ні.

Давайте розглянемо їх окремо.

Якщо вона поділяється на сторінках, це компонент.

Давайте рефакторируем об'єкт Page :: Tweetbox в компонент. Наступний фрагмент просто змінює ім'я зі сторінки :: Tweetbox на компонент :: Tweetbox . Це не відповідає на більшість наших запитань, але це необхідне початкове місце.

module Component
 
class Tweetbox
    include
Capybara::DSL
 
   
def open
      click_button
'Tweet'
   
end
 
end
end

У тестах, замість використання об'єкта підсторінки, Сторінка :: Tweetbox , ми би інстанціювали компонент Component :: Tweetbox .

Сторінки мають URL-адреси, а компоненти - ні.

Це важлива відмінність, оскільки дозволяє нам будувати кращі інструменти навколо сторінок. Якщо у нас є базовий клас сторінки , ми можемо почати підтримувати поняття URL. Нижче ми додамо простий DSL для оголошення URL-адреси сторінки, метод багаторазового використання #navigate та можливість стверджувати, що сторінка - це поточна сторінка.

class Page
 
# Our mini DSL for declaring a URL
 
def self.url(url)
   
@url = url
 
end
 
 
# We're supporting both static and dynamic URLs, so assume
 
# it's a dynamic URL if the PO is instantiated with an arg
 
def initialize(*args)
   
if args.count > 0
     
# We're initializing the page for a specific object
     
@url = self.class.instance_variable_get(:@url).(*args)
   
end
 
end
 
 
# Our reusable navigate method for all pages
 
def navigate(*args)
    page
.visit url(*args)
 
end
 
 
# An assertion we can use to check if a PO is the current page
 
def the_current_page?
    expect
(current_path).to eq(url)
 
end
 
 
private
 
 
# Helper method for calculating the URL
 
def url(*args)
   
return @url if @url
 
    url
= self.class.instance_variable_get(:@url)
    url
.respond_to?(:call) ? url.(*args) : url
 
end
 
  include
Capybara::DSL
end

Наш профіль та послідовники спеціалістів, які працюють після цього, тепер можуть використовувати базовий клас, який ми тільки що визначили. Давайте оновимо їх. Нижче ми використовуємо міні-DSL для оголошення URL-адреси вгорі. Цей DSL підтримує проходження лямбдасів для розміщення PO, який має динамічну URL-адресу. Ми можемо видалити метод #navigate з обох ОР та використати той у базовому класі Page .

Сторінка профілю, яка відновила використання базового класу Page .

class Page
 
# Our mini DSL for declaring a URL
 
def self.url(url)
   
@url = url
 
end
 
 
# We're supporting both static and dynamic URLs, so assume
 
# it's a dynamic URL if the PO is instantiated with an arg
 
def initialize(*args)
   
if args.count > 0
     
# We're initializing the page for a specific object
     
@url = self.class.instance_variable_get(:@url).(*args)
   
end
 
end
 
 
# Our reusable navigate method for all pages
 
def navigate(*args)
    page
.visit url(*args)
 
end
 
 
# An assertion we can use to check if a PO is the current page
 
def the_current_page?
    expect
(current_path).to eq(url)
 
end
 
 
private
 
 
# Helper method for calculating the URL
 
def url(*args)
   
return @url if @url
 
    url
= self.class.instance_variable_get(:@url)
    url
.respond_to?(:call) ? url.(*args) : url
 
end
 
  include
Capybara::DSL
end

Сторінка, що підписалася, відновила базовий клас Page .

class Page::Profile < Page
  url
lambda { |handle| "/#{handle}" }
 
 
def navigate_to_followers
    click_link
'Followers'
 
end
end

Нижче в тесті тепер використовуються оновлені API API. Я виключаю тест компонентів для створення нового твіту, але незабаром розпочну його вирішення.

class Page::Followers < Page
  url
lambda { |handle| "/#{handle}/followers"}
 
 
def navigate_to_tweets
    click_link
'Tweets'
 
end
end

У вищенаведеному тесті відбувається кілька речей. По-перше, ми не є жорстким кодуванням URL-адрес у самих тестах. У початковому прикладі URL-адреса сторінки профілю та URL-адреса сторінок підписників були жорстко кодовані і тому не використовувались повторно в тестах. Помістивши URL-адресу в PO, ми можемо інкапсулювати URL-адресу.

По-друге, ми використовуємо URL-адресу в межах PO_ profile_page для переходу на сторінку профілю користувача. У нашому тестовому налаштуванні ми повідомляємо браузеру переходити до URL-адреси, але вказуємо лише обробку. Оскільки наш базовий клас сторінки підтримує лямбдаси для генерування URL-адрес, ми можемо динамічно створювати URL-адресу на основі ручки.

По-третє, ми стверджуємо, що сторінка послідовників - це поточна сторінка, використовуючи трохи RSpec магії . Роблячи твердження #be_the_current_page , RSpec буде викликати метод #the_current_page? про будь-який об’єкт твердження робиться. У цьому випадку це новий примірник сторінки :: Підписники . #the_current_page? Очікується, що повернеться справжньою чи хибною, і наша версія його використовує URL-адресу, вказану в PO, для перевірки відповідності URL-адреси поточного веб-переглядача. Нижче я скопіював релевент-код із базового класу Page, який відповідає цьому твердженню.

def the_current_page?
  expect
(current_path).to eq(url)
end

Ось як ми можемо забезпечити кращу підтримку URL-адрес для ОП. Природно, що частини сторінки не мають URL-адрес, тому компоненти не мають URL-адрес. (Якщо ви ведете педантичний характер, частина сторінки може бути пов’язана з ідентифікатором фрагмента , але вони майже завжди посилаються на копіювання всередині сторінки, а не на конкретні функції.)

Сторінки мають твердження, компоненти - ні.

Загальноприйнята мудрість говорить про те, що виробники підприємств не повинні робити тверджень на сторінці. Їх слід використовувати виключно для виконання дій. Побудувавши великі системи навколо ОП, я не знайшов жодних доказів того, що це гідне правило. Суб'єктивно я помітив збільшення виразності тестів, які стверджують, що на ПЗ. Об'єктивно, і що важливіше, є можливість повторного використання аспектів PO між діями та твердженнями, як-от селектори DOM. Повторне використання коду між діями та твердженнями є важливим для збереження тестового набору DRY та вільно пов'язаного. Без висловлювань тверджень, знання про сторінку недостатньо інкапсульовано в межах ПО та розповсюджується протягом усього тестового набору.

Але є один аспект об'єктів, що не мають тверджень, які я приймаю, і це повертає нас до вирішення того, як ми управляємо компонентами.

Компоненти не повинні робити тверджень. Компонентні об'єкти повинні існувати, щоб ми могли повністю перевірити наше застосування, але бажання робити твердження на них повинно вести нас в інший шлях. Далі йдеться про прийнятне використання компонентів, оскільки ми використовуємо їх для виключно виконання дій. Тут ми припускаємо, що на компоненті tweetbox існує три методи, які дозволяють нам публікувати твіт.

describe 'the profile page' do
  let
(:profile_page) { Page::Profile.new }
  let
(:tweetbox) { Component::Tweetbox.new }
 
  before
do
    profile_page
.navigate('mikepack_')
 
end
 
  it
'shows a tweet immediately after publishing' do
   
# These three actions could be wrapped up into one helper action
   
# eg #publish_tweet(content)
    tweetbox
.open
    tweetbox
.write('What a nice day!')
    tweetbox
.submit
 
    expect
(profile_page).to have_tweet('What a nice day!')
 
end
end

У наведеному вище прикладі ми використовуємо компонент tweetbox для виконання дій над сторінкою та профілем PO, щоб робити твердження про сторінку. Ми внесли #have_tweet твердження, яке повинно знати, в якій частині сторінки знайти твіти та розширити твердження до цього селектора DOM.

Тепер, щоб показати, як не використовувати компоненти, нам просто потрібно переглянути наш перший тест. Цей тест дає твердження про вміст компонента твітбокса . Я скопіював це нижче для зручності використання.

describe 'the profile page' do
  let
(:profile_page) { Page::Profile.new }
 
  before
do
    profile_page
.navigate('mikepack_')
 
end
 
  it
'allows me to write a new tweet' do
    profile_page
.open_tweetbox
 
    expect
(page).to have_content('Compose new Tweet')
 
end
end

Після перетворення цього тесту для використання компонента твітбоксу він виглядатиме наступним чином.

describe 'the profile page' do
  let
(:profile_page) { Page::Profile.new }
 
  before
do
    profile_page
.navigate('mikepack_')
 
end
 
  it
'allows me to write a new tweet' do
    profile_page
.open_tweetbox
 
    expect
(page).to have_content('Compose new Tweet')
 
end
end

Не добре. Ми робимо твердження про компонент tweetbox .

Чому б не зробити твердження про компоненти? Насправді, ніщо не зупиняє вас, але вам все одно доведеться відповісти на питання: "про всі сторінки, які використовують цей компонент, на якій сторінці я повинен робити твердження?" Якщо ви виберете одну сторінку на іншій, прогалини в тестовому покритті не зникнуть. Якщо ви виберете всі сторінки, які містять цей компонент, набір буде надмірно повільним.

Схильність до тверджень про компоненти випливає з динамічного характеру цих компонентів. У разі компонента твітбокса натискання кнопки «новий твіт» спричиняє динамічну поведінку компонента. Натискання цієї кнопки показує модал і форму для складання твіту. Динамічна поведінка компонента реалізується за допомогою JavaScript, а тому повинна бути протестована за допомогою JavaScript. Перевіряючи JavaScript, існує один вхідний тестовий вхід з компонентом, і ми більш жорстко висвітлюємо крайові випадки компонента.

Нижче наводиться еквівалентний тест JavaScript для затвердження такої ж поведінки, як тест, описаний вище. Ви можете використовувати чайну ложку як простий спосіб інтегрувати тести JavaScript у своє середовище Rails. Я також використовую тестову рамку Mocha з бібліотекою тверджень Чая .

describe('Twitter.Tweetbox', function() {
  fixture
.load('tweetbox.html');

  beforeEach
(function() {
   
new Twitter.Tweetbox();
 
});

  it
('allows me to write a new tweet when opening the tweetbox', function() {
    $
('button:contains("Tweet")').click();

    expect
($('.modal-title').text()).to.equal('Compose new Tweet');
 
});
});

Тестуючи в JavaScript, тепер ми маємо чітку точку для тверджень. Немає більше плутанини щодо того, де слід перевірити компонент. Ми продовжуємо використовувати компоненти поряд з ОП для виконання дій у нашому пакеті прийняття, але ми не робимо тверджень про них. Ці тести працюватимуть значно швидше, ніж усе, що ми намагаємося в Capybara, і ми рухаємо логіку тестування ближче до тестового коду.

Підведенню

Не дивно, що якщо ви використовуєте веб-компоненти або дотримуєтесь структури на основі компонентів у межах свого HTML та CSS, тестування прийняття на основі компонентів - це природна придатність. Ви знайдете, що компоненти в тестах співпадають з компонентами у вашій розмітці. Це створює більшу послідовність та передбачуваність при підтримці тестового набору та формує спільний лексикон між інженерними командами.

Ваш пробіг може відрізнятися, але я знайшов цю сторінку та структуру компонентів для полегшення організаційних рішень, необхідних у кожному пакеті прийняття. Користуючись трьома простими рекомендаціями, розглянутими в цій статті, ваша команда може досягти значних успіхів у наборі вищої якості. Щасливого тестування!

Ця стаття була мило перекладена на фінську мову .

on 04/27/2015 at 08:53AM Опублікував Майк Пак 27.04.2015 о 08:53

Теги: компоненти , об’єкти сторінки , capybara , rspec , тестування , прийняття