Подсушить тесты
TL; DR
Введение
Итак, руби-рельсы, браузерное тестирование, селениум, капибара и призма.
Про селениум и капибару не буду говорить, думаю, кто занимается тестированием, про это всё знают и без меня, а вот про призму хочу сказать пару слов.
SitePrism — это DSL (Domain Specific Language), который дает описание веб-страницы, удобное для проведения тестирования. SitePrism позволяет тестировщику структурно описать страницу, выделив части, подлежащие тестированию, и опустив несущественное.
Пример. Корзина магазина:
Описание для корзины, изображенной выше:
class Cart < SitePrism::Page
set_url '/cart.html'
element :header, 'h1' # Заголовок: "Shopping cart"
sections :cart_items, 'div.cart_items' do # Массив из товаров (sections - во множественном числе)
element :name, 'div.item-name' # Имя одного товара
element :image, 'img.item-image' # Картинка товара
element :price, 'div.item-price' # Цена за единицу
element :quantity, 'div.item-quantity' # Кол-во
element :total, 'div.item-total' # Стоимость выбранных товаров
end
section :checkout, 'div.checkout' do # Раздел внизу с кнопкой (section - в единственном числе)
element :total, 'div.checkout-total' # Общая соимость заказа
element :checkout_button, 'button.checkout-button' # Кнопка
end
end
Даже человек без подготовки, поглядев на картинку и описание, легко поймёт, что чему соответствует.
Имея подобное описание, можно написать тест вроде такого:
describe 'Cart' do
it 'is correct' do
page = Cart.new
page.load
# Проверили заголовок
expect(page.header.text).to eq('Shopping Cart')
# Убедились, что товара 2
expect(page.cart_items.size).to eq(2)
# Проверили первый товар
expect(page.cart_items[0].name.text).to match('Cup')
expect(page.cart_items[0].quantity.value).to match('1')
expect(page.cart_items[0].image[:src]).to match('cup.png')
expect(page.cart_items[0].price.text).to match('19.00')
expect(page.cart_items[0].total.text).to match('19.00')
# Проверили второй товар
expect(page.cart_items[1].name.text).to match('Cap')
expect(page.cart_items[1].quantity.value).to match('2')
expect(page.cart_items[1].image[:src]).to match('cap.png')
expect(page.cart_items[1].price.text).to match('24.00')
expect(page.cart_items[1].total.text).to match('48.00')
# Проверили подвал корзины
expect(page.checkout.total.text).to match('67.99')
expect(page.checkout.checkout_button.text).to match('Checkout')
end
end
Скорее всего, это типичная картина проверки страницы в тесте. Очевидно, тест содержит избыточные, дублирующиеся слова, замедляющие как написание, так и чтение. Да, можно сократить, введя переменные, примерно так:
item = page.cart_items[0]
expect(item.name.text).to match('Cup')
expect(item.quantity.value).to match('1')
expect(item.image[:src]).to match('cup.png')
expect(item.price.text).to match('19.00')
expect(item.total.text).to match('19.00')
Но всё равно жирно и многословно.
gem prism_checker
Я решил, что можно же просто взять призму и сказать, чего мы от нее ждём, а ждём мы соответствие такой вот структуре:
{
header: 'Shopping Cart',
cart_items: [
{
name: 'Cup',
image: 'cup.png',
price: '19.00',
},
{
name: 'Cap',
image: 'cap.png',
price: '24.00',
}
],
checkout: {
total: '67.00',
checkout_button: 'Checkout'
}
}
Вся необходимая информация есть, минимальное количество слов, в тесте такая приятная легкость образовалась. Никакого жира, один рельеф, осушили по-максимуму.
Эту идею я реализовал в gem-е PrismChecker, который позволяет коротко описывать, что мы ожидаем от призмы. Вот так:
# RSpec-версия
expect(page).to be_like(
header: 'Shopping Cart',
cart_items: [
{
name: 'Cup',
image: 'cup.png',
price: '19.00',
},
{
name: 'Cap',
image: 'cap.png',
price: '24.00',
}
],
checkout: {
total: '67.00',
checkout_button: 'Checkout'
}
)
# MiniTest-версия
assert_page_like(page,
header: 'Shopping Cart',
cart_items: [
# ...
],
checkout: {
# ...
}
)
Иными словами, PrismChecker проверяет призму на соответствие некоторому хэшу.
Грубо говоря, element-у ставится в соответствие строка, section-у — хэш, а elements и sections проверяются с помощью массива.
# element
assert_page_like(page, header: 'Shopping Cart')
assert_page_like(page.header, 'Shopping Cart')
# section
assert_page_like(page, checkout: {
total: '67.00',
checkout_button: 'Checkout'
})
assert_page_like(page.checkout,
total: '67.00',
checkout_button: 'Checkout'
)
# elements, sections
assert_page_like(page, cart_items: [
{
name: 'Cup',
price: '19.00',
},
{
name: 'Cap',
price: '24.00',
}
])
В случае ошибок будет выдано сообщение такого вида:
Цветные буквы пока завезли только в RSpec, MiniTest — чёрно-белый.
Подробности
Установка
для RSpec:
# Gemfile
gem 'prism_checker_rspec'
# spec_helper.rb
require 'prism_checker_rspec'
для MiniTest:
# Gemfile
gem 'prism_checker_minitest'
# test_helper.rb
require 'prism_checker_minitest'
Примеры
Проверка секции и двух дочерних элементов:
assert_page_like(page, checkout: {
total: '67.00',
checkout_button: 'Checkout'
})
Если первый аргумент является классом SitePrism: Page, то при тестировании вначале будет выполнена проверка, что страница загружена. Элементы и секции сначала проверяются на видимость, потом на соответствие:
assert_page_like(page, button: 'Button')
# assert(page.loaded?)
# assert(page.button.visible?)
# assert_match(page.button.text, 'Button')
Если элемент является изображением, то проверяемая строка будет сравниваться с атрибутом src
. Для input и textarea проверяется value
. Checkbox или radio можно проверить следующим образом:
assert_page_like(page, checkbox: true)
assert_page_like(page, checkbox: {checked: true})
element можно сравнивать не только со строкой, но и с хэшем:
assert_page_like(page.image,
src: 'logo.png',
class: 'logo',
alt: 'Logo')
Проверить, что элемент/секция видимы/невидимы или отсутствуют:
assert_page_like(page,
header: :visible,
checkout: :invisible
)
assert_page_like(page, checkout: :absent)
Элементы и секции вначале проверяются на то, что содержат правильное количество записей:
assert_page_like(page,
items: [
'Item 1',
'Item 2'
])
# assert(page.loaded?)
# assert_equal(page.items.size, ["Item 1", "Item 2"].size)
# ...
Элементы и секции можно проверять просто на размер:
assert_page_like(page, items: 2)
# assert(page.loaded?)
# assert_equal(page.items.size, 2)
Со строкой можно сравнивать не только element, но и page, section, sections, elements:
assert_page_like(page, 'Shopping Cart')
# assert(page.loaded?)
# assert_match(page.text, 'Shopping Cart')
Вместо строки можно использовать регулярные выражения:
assert_page_like(page, /Shopping Cart/)
Если нужно проверить, что текст элемента есть пустая строка, то может возникнуть проблема. По умолчанию проверяется вхождение подстроки, а пустая строка входит в любую строку:
assert_page_like(page, header: '') # Всегда сработает, даже если header не пуст
Эту проблему можно решить с regexp или специальной проверкой:
assert_page_like(page, header: /^$/)
assert_page_like(page, header: :empty)
По умолчанию строки сравниваются путем поиска подстроки, но можно настроить гем на точное соответствие:
assert_page_like(page, header: 'Cart')
# будет проверено вот так:
header.include?('Cart')
PrismChecker.string_comparison = :exact # Точное соответствие
assert_page_like(page, header: 'Cart')
# будет проверено вот так:
header == 'Cart'
Еще примеры можно найти на странице проекта, на гитхабе.
Итого
Gem PrismChecker упрощает написание и чтение браузерных тестов и бережет нервы тестировщиков.
Буду рад услышать отзывы, кртику, баги.