Знакомство с Presto — Заключительная часть — Тестирование

в 12:56, , рубрики: ruby, Веб-разработка, метки:

Начну с банального примечания — данная утилита является лишь дополнением к существующим тест фреймворком, а не их заменой.
И ничего особенного в ней нет, просто очень удобно работать.

Мотивация:

  1. Визуальный контакт. Я хочу чтобы спецификации физически находились рядом, в том же файле или папке, но никак не в амбаре.
  2. Умные браузеры. Когда я пишу спецификацию для определённого action-а, браузер должен определять адрес автоматически.
  3. Никаких хаков. Тестируемые объекты и базовые классы Ruby должны остаться в нетронутом состоянии.

Часть Первая | Часть Вторая | Часть Третья | Часть Четвёртая | Часть Пятая

Presto Source code: https://github.com/slivu/presto
PrestoTest Source code: https://github.com/slivu/presto-test
IRC: #prestorb on irc.freenode.net


Оглавление


Спецификации

Каждый action может иметь множество спецификаций.
Первый аргумент это имя/описание спецификации.
Последующие аргументы это имя action-а и/или опции в виде хэша.

# action
def details
  # some logic
end

ctrl.spec 'Testing links', :details do
  # some logic
end

ctrl.spec 'Testing banners', :details do
  # some logic
end

Спецификации можно пропускать если передать опцию :skip

ctrl.spec 'Skipping for now', skip: true do
  # code here will not be executed
end

Если задать имя action-а(через 2-й аргумент), браузеры внутри спецификации будут обращаться к заданному action-у

ctrl.spec 'Testing CRUD - edit', :edit do
  get 100 # сделает запрос по адресу /edit/100
end

Если action не задавать, браузеры будут обращаться к :index

ctrl.spec 'OverallTesting' do
  get # сделает запрос по адресу /index
end

оглавление

Сценарии

Сценарии вовсе не обязательны но они помогают разбивать спецификации на логические части.

ctrl.spec 'Testing theory of relativity' do
  Suppose "I'm Superman" do
    And "I can fly" do
      But "I can not pry" do
        When "I'm landing" do
          is("it real to keep my ass?").kind_of? Random
        end
      end
    end
  end
end

Сценарии начинаются с заглавной буквы и должны иметь имя/описание.
Последующие аргументы это имя action-а и/или опции в виде хэша.

Возможные сценарии:

  • Given
  • When
  • Then
  • It
  • If
  • Let
  • Say
  • Assume
  • Suppose
  • And
  • But
  • Should

Нужны ещё? Предлагаете — рассмотрим, добавим.

Сценарии можно пропускать если передать опцию :skip

ctrl.spec 'SomeSpec' do

  Given 'user clicked register', skip: true do
  end
end

Если задать имя action-а(через 2-й аргумент), браузеры внутри сценария будут обращаться к нему

ctrl.spec 'Buying Workflow', :buy do

  get 'Coolest-Product-Ever' # сделает запрос по адресу /buy/Coolest-Product-Ever

  # custom action for scenarios
  Suppose 'user choose to create a new account', :register do
    visit # сделает запрос по адресу /register
  end

end

Если action не задавать, сценарий будет обращаться к action-у заданный spec-ом или родительским сценарием

ctrl.spec 'Buying Workflow', :buy do

  get 'Coolest-Product-Ever' # сделает запрос по адресу /buy/Coolest-Product-Ever

  # custom action for scenarios
  Suppose 'user choose to create a new account', :register do

    visit # сделает запрос по адресу /register

    When 'user click "Personal Account"' do
      visit 'personal-account' # сделает запрос по адресу /register/personal-account
    end

  end

  # this scenario using action set by spec
  If 'user has a coupon' do
    visit 'i-have-a-coupon' # сделает запрос по адресу /buy/i-have-a-coupon
  end

end

оглавление

Тесты

Для декларации тестов PrestoTest использует одно единственное правило — «Правило Двух Скобок»
И это единственное правило которое нужно запомнить.
Потому что ВСЁ что следует после скобок делается на чисто натуральном Ruby,
без хаков и жонглирования с объектами/классами.

Логика предельно проста — тестируемый объект должен находиться внутри скобок, круглых или фигурных.
Допустим foo тестируемый объект а bar ожидаемое значение.
По правилу двух скобок это будет выглядеть так:

is(foo) == bar

И дальше даём волю воображению…

is?(foo) > bar
is(foo) >= bar
is?(foo) < bar
is(foo) <= bar
does(foo) =~ bar
is?(foo).instance_of? bar
does?(foo).respond_to? bar
# etc

Вот как это выглядит на деле:
app.rb

require 'presto'
require 'presto-test'
                   
 class App
   include Presto::Api
   http.map

   ctrl.spec 'BasicTests' do

     def smells_like_a_pizza? obj
       obj.to_s =~ /#{Regexp.union 'pizza', 'olives', 'cheese'}/i
     end

     def contain_cheese? obj
       obj.to_s =~ /cheese/i
     end

     Should 'pass' do

       foo, bar = 1, 1
       is(foo) == bar
       refute(foo) > bar

       foo, bar = 1, 2
       false?(foo) == bar
       is?(foo) <= bar

       foo, bar = 'foo'.freeze, 'bar'
       is(foo).frozen?
       refute(bar).frozen?

       foo = "Hi, I'm Duck the Greatest! Quack! Quack!"
       does(foo).looks_like_a_duck?
       does(foo).quacks?

       bar = "I'm a pizza with olives and lot of cheese!'"
       does(bar).smells_like_a_pizza?
       does(bar).contain_cheese?

       foo = 1
       bar = [foo, 2, 3]
       is(bar.size) == 3
       does(bar).respond_to? :include?
       does(bar).include? foo

       does { throw :some, :test }.throw_symbol? :some, :test
       expect { something risky }.to_raise NoMethodError

     end

     Should 'fail' do
       foo, bar = 'some string', :some_symbol
       expect(foo) == bar
     end

     Should 'fail' do
       is('foo').martian?
     end

     Should 'fail' do
       does { 1+1 }.throw_symbol?
     end

     Should 'fail' do
       refute { something risky }.raise_error NoMethodError
     end

   end

   private

   def looks_like_a_duck? obj
     obj.to_s =~ /duck/i
   end

   def quacks? obj
     obj.to_s =~ /quack/i
   end

 end
 app = Presto::App.new
 app.specs.run
 puts app.specs.to_s

ruby app.rb
image

Просто и доступно.

оглавление

Встроенные helper-ы

raise_error

Работает только с блоками.
Если вызвать без аргументов, фреймворк ожидает что блок вернёт ошибку любого типа:

expect{ some bad code here }.to_raise_error
# - passed

expect{ 'some bad code here' }.to_raise_error
# - failed

Если вызвать с одним аргументом и аргумент является классом, фреймворк ожидает что блок вернёт ошибку того же типа что и заданный класс:

does{ some bad code here }.raise? NoMethodError
# - passed
does{ some bad code here }.raise? SomeCustomError
# - failed

Если вызвать с одним аргументом и аргумент является строкой или регулярным выражением, фреймворк ожидает что блок вернёт ошибку с сообщением содержащей заданный текст:

does{ some bad code here }.raise? /bad code/
# - passed
does{ some bad code here }.raise? 'bad code'
# - passed
does{ some bad code here }.raise? 'blah'
# - failed

Если вызвать с двумя аргументами, из которых один является классом а другой строкой или регулярным выражением,
фреймворк ожидает что блок вернёт ошибку того же типа что и заданный класс и с сообщением содержащей заданный текст:

does{ some bad code here }.raise? NoMethodError, /bad code/
# - passed
does{ some bad code here }.raise? SomeCustomError, /bad code/
# - failed
does{ some bad code here }.raise? NoMethodError, 'blah'
# - failed

Список алиасов:

  • raise?
  • raise_error?
  • to_raise
  • to_raise_error

throw_symbol

Работает только с блоками.
Если вызвать без аргументов, фреймворк ожидает что блок передаст контроль используя любой символ:

expect{ throw :back_to_future }.throw_symbol
# - passed
expect{ throw :anywhere }.throw_symbol
# - passed

Если задан первый аргумент, фреймворк ожидает что блок передаст контроль используя заданный символ:

does{ throw :begining_of_times }.throw_symbol? :begining_of_times
# - passed
does{ throw :begining_of_times }.throw_symbol? :far_far_away
# - failed

Если задан и второй аргумент, фреймворк ожидает что блок передаст контроль используя заданный символ и также передаст заданное значение:

does{ throw :begining_of_times, 'N bc' }.throw_symbol? :begining_of_times, 'N bc'
# - passed
does{ throw :begining_of_times, 'N bc' }.throw_symbol? :begining_of_times, 'today'
# - failed

Список алиасов:

  • throw?
  • throw_symbol?
  • to_throw
  • to_throw_symbol

оглавление

Собственные helper-ы

Можно декларировать внутри спецификаций или самих controller-ов

Если декларировать внутри спецификации, они будут доступны только для данной спецификации.

ctrl.spec "SomeSpec" do
  def looks_like_jack? obj
    obj =~ /jack/
  end

  does('Jack Daniels').looks_like_jack?
  # - passed
  does('Captain Jack').looks_like_jack?
  # - passed
  does('Britney Spears').looks_like_jack?
  # - failed
end

Если методы декларировать внутри controller-а, они будут доступны для всех спецификаций.
Внутри controller-а лучше ставить данные методы в приват, чтобы они не были доступны через веб.

Список алиасов

  • is
  • is?
  • are
  • are?
  • does
  • does?
  • expect
  • assert

Нужны ещё? Предлагаете — рассмотрим, добавим.

оглавление

Браузеры

Браузеры внутри спецификации/сценария будут по умолчанию обращаться к заданному action-у.

class Members
  include Presto::Api
  http.map :members

  def login
    # some logic
  end

  ctrl.spec "Testing Login", :login do

    get # сделает GET запрос по адресу /members/login
  end
end

Если action-у нужны аргументы, передавайте их через браузер.

def edit id

end

ctrl.spec 'Testing CRUD / edit', :edit do

  get 10 # сделает GET запрос по адресу /edit/10
end

def menu position
end

ctrl.spec 'Testing Menus', :menu do

  get 'top' # сделает GET запрос по адресу /menu/top
end

Также можно передать список HTTP параметров.

def menu position
end

ctrl.spec 'Testing Menus', :menu do

  get 'top', :color => 'red' # сделает GET запрос по адресу /menu/top?color=red
end

Однако, если первый аргумент является символом, браузер примет его за имя action-а и обратиться к нему!

def menu position
end
def banners position
end

ctrl.spec 'Testing Menus', :menu do

  get 'top'            # сделает GET запрос по адресу /menu/top
  get :banners, 'top'  # сделает GET запрос по адресу /banners/top
end

Дальше — лучше… если первый аргумент является controller-ом, браузер обращается к action-ам внутри заданного controller-а.
В данном случае, второй аргумент должен быть action-ом а все последующие — параметрами с которыми action будет выполнятся.

class Index
  include Presto::Api
  http.map :cms

  def menu scope = nil
  end
end
class Forum
  include Presto::Api
  http.map :forum

  def index
  end

  ctrl.spec 'Top menu' do
    get Index, :menu, :forum # сделает GET запрос по адресу /cms/menu/forum
  end
end

Сделано это для того чтобы сохранять работоспособность тестов даже когда controller-ы меняют адреса.
Если в данном примере Index поменяет адрес с /cms на /pages, в тестах ничего менять не надо.

оглавление

Rack::Test Браузер

Стандартные запросы — get, post, put, delete, options, head

Вывода результата можно добиться двумя способами:
1: через назначенную переменную

response = get
# HTML читаем через response.body
# header-ы  - через response.headers

2: через browser.last_response

get
# HTML читаем через browser.last_response.body
# header-ы  - через browser.last_response.headers

Тоже самое с post

Если нужны методы из Rack::Test::Methods, используете browser

response = get
browser.last_response.follow_redirect!

browser.header "User-Agent", "Firefox"

browser.set_cookie
browser.clear_cookies

# etc

Сделано это для того чтобы можно было совместно использовать Rack::Test, Capybara и может другие браузеры.

Ajax запросы

Аналогично стандартному запросу, только добавляем xhr_ префикс

xhr_get
xhr_post

Результаты запроса читаем также как при get и post

JSON запросы

Если action возвращает JSON объект, используйте get_json / post_json.
JSON объект будет доступен через response.json

def create
  # some logic
  {status: 1, message: 'success'}.to_json
end

ctrl.spec 'Creating items', :create do
  response = get_json
  # response.body: '{"status":1,"message":"success"}' [String]
  # response.json: {"status"=>1, "message"=>"success"} [Hash]
end

Если нужно сделать запрос к JSON action-у через Ajax, используете xhr_ префикс:

xhr_get_json
xhr_post_json

Авторизация

Если action запрашивает авторизацию, перед запросом используем authorize:

authorize 'admin', 'reallySecretPassword'
get

Если action запрашивает digest авторизацию, перед запросом используем digest_authorize:

digest_authorize 'admin', 'reallySecretPassword'
get

Чтобы отменить авторизацию, используете reset! или reset_session!

authorize 'admin', 'reallySecretPassword'
get # запрос проходит с авторизацией

reset!
get # запрос проходит без авторизации

оглавление

Capybara Браузер

Так как Capybara является опциональным браузером, в поставку он не включён.
Следовательно надо его установить/конфигурировать перед использованием.

Чтобы использовать во всех спецификациях, надо его включить на уровне приложения:

app = Presto::App.new
app.specs.run :capybara => true
puts app.specs.to_s

Чтобы использовать Capybara только для определённого spec-а, задаём опцию :capybara только для данного spec-а:

ctrl.spec 'SomeSpec', :some_action, :capybara => true do
  # some logic
end

Всё что сказано выше на счёт автоматической адресации является действительным и для visit

ctrl.spec 'SomeSpec', :some_action, :capybara => true do
  visit                        # сделает запрос по адресу /some_action
  visit 100                    # сделает запрос по адресу /some_action/100
  visit 100, :color => 'red'   # сделает запрос по адресу /some_action/100?color=red
end

оглавление

Хуки

before/after — выполняем определённые действия перед/после каждым тестом

ctrl.spec 'SomeSpec' do
  before do
    @page = Model::Page.new
  end

  after do
    @page.destroy
  end
end

Хуки декларированные на уровне spec-а будут выполняться во всех сценариях.
Сценарии могут иметь также и собственные хуки.
В данном случае будут выполняться сначала унаследованные от spec-а хуки а потом собственные.

ctrl.spec 'SomeSpec' do

  before do
    @page = Model::Page.new
  end

  after do
    @page.destroy
  end

  Should 'run a hook that will modify @page state' do
    before do
      @page.status = 1
    end
  end
end

Хуки декларированные на уровне сценария являются сугубо персональными и НЕ наследуются потомками.

ctrl.spec 'SomeSpec' do

  before do
    @page = Model::Page.new
  end

  after do
    @page.destroy
  end

  Should 'run a hook that will modify @page state' do

    before do
      @page.status = 1
    end
    # here tests will run top level hooks + scenario hooks

    Should 'run only spec hooks' do
      # here tests will run only top level hooks
    end

  end
end

оглавление

Статус последнего теста

passed? — вернёт true если последний тест прошёл успешно
failed? — вернёт true если последний тест провалился

ctrl.spec 'SomeSpec' do

  is(1) == 1
  passed? # true
  failed? # false

  is(1) == 0
  passed? # false
  failed? # true
end

оглавление

Стандартный Вывод

output — выводим дополнительные детали в текущем контексте

Иногда нужно выводить дополнительные детали текущего действия.
puts и компания выведут информацию где-то на полях.
А вот output выведет именно в том месте где действие происходит, да ещё с подсветкой.

ctrl.spec 'Creating new account', :register do

  data = {name: rand, email: rand}
  output 'sending request ...'
  
  result = post data
  is?(result.body) == 'success'
  
  if passed?
    output 'account created!', :green
  end
end

Список доступных цветов:

  • red
  • green
  • yellow
  • blue
  • magenta
  • cyan
  • white

error — добавляем детали к сообщению об ошибке последнего провалившегося теста

ctrl.spec 'Creating new account', :register do

  data = {name: rand, email: rand}
  result = post data
  is?(result.body) == 'success'
  if failed?
    error 'data provided: %s' % data
    error 'even more details'
    error 'and maybe some debugging'
    error 'etc...'
  end
  # выведет стандартное сообщение об ошибке + детали добавленные вручную
end

оглавление

Применение

Для начала нужно установить presto-test:

gem install presto-test

Потом просто подгружаем его вместе с другими гемами:

require 'presto'
require 'presto-test'

class App
  include Presto::Api
  http.map

  def index
    # some logic
  end

  ctrl.spec 'SomeSpec' do
    # some logic
  end
end

# testing app
app = Presto::App.new
app.specs.run
if app.specs.passed?
  app.run
else
  puts app.specs.to_s
end

Можно также тестировать отдельно взятые controller-ы или slice-ы

class News
  include Presto::Api
  http.map :news

  ctrl.spec 'SomeSpec' do
    # some logic
  end
end

module Forum

  class Members
    include Presto::Api
    http.map :members

    ctrl.spec 'SomeSpec' do
      # some logic
    end
  end
  
  class Posts
    include Presto::Api
    http.map :posts

    ctrl.spec 'SomeSpec' do
      # some logic
    end
  end
end

# testing News Controller
news_specs = Presto::App.mount(News).specs
news_specs.run
puts news_specs.to_s

# testing Forum Slice
forum_specs = Presto::App.mount(Forum).specs
forum_specs.run
puts forum_specs.to_s

А результаты можно выводить по частям

  • #passed? — вернёт true если все тесты прошли успешно
  • #output — данные о ходе выполнения тестов
  • #skipped_specs
  • #skipped_scenarios
  • #failed_tests
  • #summary

app = Presto::App.new
specs = app.specs

specs.run

if specs.passed?
  puts specs.summary.to_s
else
  puts specs.output.to_s
  puts specs.failed_tests.to_s
end

if specs.skipped_specs.size > 0
  puts specs.skipped_specs.to_s
end

if specs.skipped_scenarios.size > 0
  puts specs.skipped_scenarios.to_s
end

оглавление

Автор: slivu

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


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