А чтобы тестировать не отходя от кассы нужен фреймворк который внедряется в код
но никак не влияет на его работу.
Именно это делает Spine — позволяет писать тесты рядом с кодом никак не влияя на работу приложения.
Почему Spine?
Потому что «Specs Inline» и потому что(imho) для рационального ПО, тесты играют роль позвоночника.
Многим это статья может показаться повтором и они будут отчасти правы,
так как данная статья основана на пятой части знакомства с Presto.
А сам Spine вырос из и стал на замену PrestoTest фреймворка.
И зачем повторять то что уже написано?
Просто Spine существенно отличается от PrestoTest и соответственно данная статья тоже отличается от предыдущей, процентов на 80.
Да и представлять новый гем в пятой части знакомства с Presto как-то не корректно.
И да, статья не претендует на большие плюсы. Если вам данная методология не по вкусу,
минусовать не зачем, просто игнорируйте её и используете ваш любимый тест-фреймворк. Спасибо.
Мотивация:
- Визуальный контакт. Я хочу писать спецификации одновременно с кодом
и чтобы они физически находились рядом, в том же файле или папке, но никак не в амбаре. - Простые вещи должны остаться простыми.
foo.should == bar
никак не заменитfoo == bar
- Я не хочу ни запоминать список синтетических заменителей простых вещей
ни работать с документацией под рукой. - Никаких хаков. Тестируемые объекты и базовые классы Ruby должны остаться в
первоначальном состоянии.
В двух словах ...
# Install
$ gem install spine
# Load
require 'spine'
# Use
class App
def body
'some text'
end
# writing tests
Spine.vertebra 'GenericTest' do
Should 'do a simple test' do
body = App.new.body
is(body) == 'some text'
# - passed
does(body) =~ /text/
# - passed
end
end
end
# running tests
puts Spine.run
А отсюда по подробнее
Source code: https://github.com/slivu/spine
IRC: #prestorb on irc.freenode.net
- Задания
- Спецификации
- Сценарии
- Тесты
- Встроенные helper-ы
- Собственные helper-ы
- Хуки
- Статус последнего теста
- Стандартный Вывод
- Применение
ТЗ декларируются через
Spine.task
Имя можно задать через 1ый аргумент.
Опции через 2ой или через 1ый, если задание не нуждается в имени.
# Defining tasks:
class TestedClass
# define your methods
Spine.task :test_integers do
# test your methods
end
Spine.task :test_strings do
# test your methods
end
Spine.task :yet_another_task do
# test your methods
end
end
# Running tasks:
# run all tasks
Spine.run
# run tasks starting with "test"
Spine.run /^test/
# run only "test_integers" task
Spine.run :test_integers
Задания можно пропускать если передать опцию :skip
Spine.task :some_task, skip: true do
# tests here will not be executed
end
оглавление↑
Спецификации
Спецификации начинаются с заглавной буквы и должны иметь имя/описание.
Первый аргумент это имя/описание спецификации.
2ой аргумент это опции в виде хэша.
Spine.task do
Spec 'Testing links' do
# some logic
end
Spec 'Testing banners' do
# some logic
end
end
Спецификации можно пропускать если передать опцию :skip
Spec 'Skipping for now', skip: true do
# tests here will not be executed
end
оглавление↑
Сценарии
Сценарии вовсе не обязательны но они помогают разбивать спецификации на логические части.
Spine.task do
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
end
Сценарии начинаются с заглавной буквы и должны иметь имя/описание.
Последующие аргументы это опции в виде хэша.
Возможные сценарии:
- Given
- When
- Then
- It
- If
- Let
- Say
- Assume
- Suppose
- And
- Nor
- But
- However
- Should
Нужны ещё? Предлагаете — рассмотрим, добавим.
Сценарии можно пропускать если передать опцию :skip
Given 'user clicked register', skip: true do
# tests here will not be executed
end
оглавление↑
Тесты
Для декларации тестов Spine использует одно единственное правило — «Правило Двух Скобок»
И это единственное правило которое нужно запомнить.
Потому что ВСЁ что следует после скобок делается на чисто натуральном 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 'spine'
class SomeClass
module TestingHelper
def looks_like_a_duck? obj
obj.to_s =~ /duck/i
end
def quacks? obj
obj.to_s =~ /quack/i
end
end
Spine.task 'SomeTask' do
Spec 'BasicTests' do
include TestingHelper
def smells_like_a_pizza? obj
obj.to_s =~ /#{Regexp.union 'pizza', 'olives', 'cheese'}/i
end
def contain? food, ingredient
food =~ /#{ingredient}/
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?
pizza = "I'm a pizza with olives and lot of cheese!'"
does(pizza).smells_like_a_pizza?
does(pizza).contain? 'olives'
does(pizza).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_error
end
Should 'fail' do
foo, bar = 'some string', :some_symbol
expect(foo) == bar
is(1) == 1
end
Should 'fail' do
does { 1+1 }.throw_symbol?
end
Should 'fail' do
refute { something risky }.raise_error
end
end
end
end
puts Spine.run
ruby app.rb
Просто и доступно.
Список алиасов
- is
- is?
- are
- are?
- does
- does?
- expect
- assert
Нужны ещё? Предлагаете — рассмотрим, добавим.
оглавление↑
Встроенные 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-ы
Можно включать через
include ModuleName
module SomeHelper
def between? val, min, max
(min..max).include? val
end
end
Spine.task do
include SomeModule
is(10).between? 0, 100
# - passed
is(10).between? 1, 5
# - failed
end
или декларировать внутри заданий
Spine.task do
def between? val, min, max
(min..max).include? val
end
is(10).between? 0, 100
# - passed
is(10).between? 1, 5
# - failed
end
оглавление↑
Хуки
Хуки декларированные на уровне заданий будут выполняться во всех спецификациях и сценариях внутри задания
Spine.task do
before do
@page = Model::Page.new
end
after do
@page.destroy
end
# any test inside any spec/scenario will execute this hooks
end
Хуки декларированные на уровне спецификации будут выполняться только внутри данной спецификации
Spine.task do
Spec 'SomeSpec' do
before do
@page = Model::Page.new
end
after do
@page.destroy
end
# this hooks will be executed only by tests inside current spec and ignored on other specs.
end
end
Хуки декларированные на уровне сценария будут выполняться внутри данного сценария и внутри вложенных сценариев
Spine.task do
@page = Model::Page.first
Spec 'SomeSpec' do
Should 'run a hook that will modify @page status' do
before do
@page.status = 1
# this will be executed only inside current scenario and its children
end
And 'this scenario will modify @page status too' do
end
end
However 'this scenario wont modify @page status' do
end
end
end
оглавление↑
Статус последнего теста
passed? — вернёт true если последний тест прошёл успешно
failed? — вернёт true если последний тест провалился
is(1) == 1
passed? # true
failed? # false
is(1) == 0
passed? # false
failed? # true
оглавление↑
Стандартный Вывод
"o" — выводим дополнительные детали в текущем контексте
Иногда нужно выводить дополнительные детали текущего действия.
"puts
" и компания выведут информацию где-то на полях.
А вот "o
" выведет именно в том месте где действие происходит, да ещё с подсветкой.
Spec 'Creating new account' do
data = {name: rand, email: rand}
o 'sending request ...'
result = post '/', data
is?(result.body) == 'success'
if passed?
o.success 'account created!'
end
if failed?
o.error 'was unable to create account'
o.warn 'sent data: %s' % data
end
end
оглавление↑
Применение
Для начала нужно установить Spine:
$ gem install spine
Потом просто подгружаем его вместе с другими гемами:
require 'spine'
class App
Spine.task do
Spec 'SomeSpec' do
# some logic
end
end
end
puts Spine.run
Можно также выполнять задания по выбору
class News
Spine.task News do
# some logic
end
end
module Forum
class Members
Spine.task Forum::Members do
# some logic
end
end
class Posts
Spine.task Forum::Posts do
# some logic
end
end
end
# testing News Controller
puts Spine.run News
# testing Forum Members
puts Spine.run Forum::Members
# testing Forum Posts
puts Spine.run Forum::Posts
# testing all Forum classes
puts Spine.run /^Forum/
А результаты можно выводить по частям
- #passed? — вернёт true если все тесты прошли успешно
- #output — данные о ходе выполнения тестов
- #skipped_tasks
- #skipped_specs
- #skipped_scenarios
- #failed_tests
- #summary
specs = Spine.run
if specs.passed?
puts specs.summary
else
puts specs.output
puts specs.failed_tests
end
if specs.skipped_specs.size > 0
puts specs.skipped_specs
end
if specs.skipped_scenarios.size > 0
puts specs.skipped_scenarios
end
Автор: slivu