Подсушить тесты

TL; DR

11c6dac0f4dce599519e4769ff015ffe.png

Введение

Итак,  руби-рельсы,  браузерное тестирование,  селениум,  капибара и призма.

Про селениум и капибару не буду говорить,  думаю,  кто занимается тестированием,  про это всё знают и без меня,  а вот про призму хочу сказать пару слов.

SitePrism — это DSL (Domain Specific Language),  который дает описание веб-страницы,  удобное для проведения тестирования. SitePrism позволяет тестировщику структурно описать страницу,  выделив части,  подлежащие тестированию,  и опустив несущественное.

Пример. Корзина магазина:

004a381a4d461d014d884057a4b7ddea.png

Описание для корзины,  изображенной выше:

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')

Но всё равно жирно и многословно.

126522c1423fdd8a07151813311320b7.jpg

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,  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 упрощает написание и чтение браузерных тестов и бережет нервы тестировщиков.

Буду рад услышать отзывы,  кртику,  баги.

© Habrahabr.ru