Начну с банального примечания — данная утилита является лишь дополнением к существующим тест фреймворком, а не их заменой.
И ничего особенного в ней нет, просто очень удобно работать.
Мотивация:
- Визуальный контакт. Я хочу чтобы спецификации физически находились рядом, в том же файле или папке, но никак не в амбаре.
- Умные браузеры. Когда я пишу спецификацию для определённого action-а, браузер должен определять адрес автоматически.
- Никаких хаков. Тестируемые объекты и базовые классы Ruby должны остаться в нетронутом состоянии.
Часть Первая | Часть Вторая | Часть Третья | Часть Четвёртая | Часть Пятая
Presto Source code: https://github.com/slivu/presto
PrestoTest Source code: https://github.com/slivu/presto-test
IRC: #prestorb on irc.freenode.net
- Спецификации
- Сценарии
- Тесты
- Встроенные helper-ы
- Собственные helper-ы
- Браузеры
- Rack::Test Браузер
- Capybara Браузер
- Хуки
- Статус последнего теста
- Стандартный Вывод
- Применение
Каждый 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
Просто и доступно.
оглавление↑
Встроенные 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