- PVSM.RU - https://www.pvsm.ru -

Пишем тесты здесь и сейчас, иначе возникает большая вероятность откладывания на лучшие времена

А чтобы тестировать не отходя от кассы нужен фреймворк который внедряется в код
но никак не влияет на его работу.
Именно это делает Spine [1] — позволяет писать тесты рядом с кодом никак не влияя на работу приложения.

Почему Spine?
Потому что «Specs Inline» и потому что(imho) для рационального ПО, тесты играют роль позвоночника.

Многим это статья может показаться повтором и они будут отчасти правы,
так как данная статья основана на пятой части знакомства с Presto.
А сам Spine вырос из и стал на замену PrestoTest фреймворка.

И зачем повторять то что уже написано?
Просто Spine существенно отличается от PrestoTest и соответственно данная статья тоже отличается от предыдущей, процентов на 80.
Да и представлять новый гем в пятой части знакомства с Presto как-то не корректно.

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

Мотивация:

  1. Визуальный контакт. Я хочу писать спецификации одновременно с кодом
    и чтобы они физически находились рядом, в том же файле или папке, но никак не в амбаре.
  2. Простые вещи должны остаться простыми.
    foo.should == bar никак не заменит foo == bar
  3. Я не хочу ни запоминать список синтетических заменителей простых вещей
    ни работать с документацией под рукой.
  4. Никаких хаков. Тестируемые объекты и базовые классы 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 [1]
IRC: #prestorb on irc.freenode.net


Оглавление


Задания

ТЗ декларируются через 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

оглавление [12]

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

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

Первый аргумент это имя/описание спецификации.
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

оглавление [12]

Сценарии

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

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

оглавление [12]

Тесты

Для декларации тестов 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

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

оглавление [12]

Встроенные 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

оглавление [12]

Собственные 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

оглавление [12]

Хуки

Хуки декларированные на уровне заданий будут выполняться во всех спецификациях и сценариях внутри задания

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

оглавление [12]

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

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

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

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

оглавление [12]

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

"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

оглавление [12]

Применение

Для начала нужно установить 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

оглавление [12]

Автор: slivu


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ruby/5516

Ссылки в тексте:

[1] Spine: https://github.com/slivu/spine

[2] Задания: #tasks

[3] Спецификации: #specs

[4] Сценарии: #scenarios

[5] Тесты: #tests

[6] Встроенные helper-ы: #helpers

[7] Собственные helper-ы: #custom-helpers

[8] Хуки: #hooks

[9] Статус последнего теста: #last-test-status

[10] Стандартный Вывод: #stdout

[11] Применение: #deploy

[12] оглавление: #top