DRY(Don’t Repeat Yourself) — один из краеугольных принципов современной разработки, а особенно в среде ruby-программистов. Но если при написании обычного кода повторяющиеся фрагменты обычно легко можно сгруппировать в методы или отдельные модули, то при написании тестов, где повторяющегося кода порой еще больше, это сделать не всегда просто. В данной статье содержится небольшой обзор средств решения подобных проблем при использовании BDD-фреймворка RSpec.

1. Shared Examples


Самый известный и часто используемый метод создания многократно используемого кода для Rspec. Отлично подходит для тестирования наследования классов и включений модулей.

shared_examples "coolable" do
  let(:target){described_class.new}

  it "should make cool" do
    target.make_cool
    target.should be_cool
  end
end

describe User do
  it_should_behave_like "coolable"
end

Кроме того Shared Example Groups обладают и некоторым дополнительным функционалом, что делает их гораздо более гибкими в использовании: передача параметров, передача блока и использование let в родительской группе для определения методов.

shared_examples "coolable" do |target_name|
  it "should make #{ target_name } cool" do
    target.make_cool
    target.should be_cool
  end
end

describe User do
  it_should_behave_like "coolable", "target user" do
    let(:target){User.new}
  end
end

Подробнее о том, где и как будут доступны определенные методы, можно прочитать у Дэвида Челимски[2].

2. Shared Contexts


Данная фича несколько малоизвестна в силу своей относительной новизны(появилась в RSpec 2.6) и узкой области применения. Наиболее подходящей ситуацией для использования shared contexts является наличие нескольких спеков, для которых нужны одинаковые начальные значения или завершающие действия, обычно задаваемые в блоках before и after. На это намекает и документация:

shared_context "shared stuff", :a => :b do
  before { @some_var = :some_value }
  def shared_method
    "it works"
  end
  let(:shared_let) { {'arbitrary' => 'object'} }
  subject do
    'this is the subject'
  end
end

Очень удобной вещью в shared_context является возможность их включения по метаинформации, заданной в блоке describe:

shared_context "shared with somevar", :need_values => 'some_var' do
  before { @some_var = :some_value }
end

describe "need som_var", :need_values => 'some_var' do
  it “should have som_var” do
    @some_var.should_not be_nil
  end
end


3. Фабрики объектов


Еще один простой, но очень важный пункт.

@user = User.create(
  :email => ‘example@example.com’,
  :login => ‘login1’,
  :password => ‘password’,
  :status => 1,
  …
)

Вместо многократного написания подобных конструкций следует использовать гем factory_girl или его аналоги. Преимущества очевидны: уменьшается объем кода и не нужно переписывать все спеки, если вы решили поменять status на status_code.

4. Собственные матчеры


Возможность определять собственные матчеры — одна из самых крутых возможностей в RSpec, благодаря которой можно нереально повысить читабельность и элегантность ваших спеков. Сразу пример.
До:
it “should make user cool” do
  make_cool(user)
  user.coolness.should > 100
  user.rating.should > 10
  user.cool_things.count.should == 1
end

После:
RSpec::Matchers.define :be_cool do
  match do |actual|       
    actual.coolness.should > 100 && actual.rating.should > 10 && actual.cool_things.count.should == 1
  end
end

it “should make user cool” do
  make_cool(user)
  user.should be_cool
end

Согласитесь, стало в разы лучше.
RSpec позволяет задавать сообщения об ошибках для собственных матчеров, выводить описания и выполнять чейнинг, что делает матчеры гибкими настолько, что они просто ничем не отличаются от встроенных. Для осознания всей их мощи, предлагаю следующий пример[1]:

RSpec::Matchers.define :have_errors_on do |attribute|
  chain :with_message do |message|
    @message = message
  end
  match do |model|
    model.valid?
    @has_errors = model.errors.key?(attribute)
    if @message
      @has_errors && model.errors[attribute].include?(@message)
    else
      @has_errors
    end
  end
  failure_message_for_should do |model|
    if @message
      "Validation errors #{model.errors[attribute].inspect} should include #{@message.inspect}"
    else
      "#{model.class} should have errors on attribute #{attribute.inspect}"
    end
  end
  failure_message_for_should_not do |model|
    "#{model.class} should not have an error on attribute #{attribute.inspect}"
  end
end


5. Однострочники


RSpec предоставляет возможность использования однострочного синтаксиса при написании простых спеков.

Пример из реального opensource-проекта(kaminari):
context 'page 1' do
  subject { User.page 1 }
    it { should be_a Mongoid::Criteria }
    its(:current_page) { should == 1 }
    its(:limit_value) { should == 25 }
    its(:total_pages) { should == 2 }
    it { should skip(0) }
  end
end

Явно гораздо лучше, чем:
context 'page 1' do
  before :each do
    @page = User.page 1
  end     
  
  it  “should be a Mongoid criteria” do
    @page.should be_a Mongoid::Criteria
  end

  it “should have current page set to 1” do
    @page.current_page.should == 1
  end
   ….
  #etc


6. Динамически создаваемые спеки


Ключевым моментом здесь является то, что конструкция it (как впрочем и context и describe) является всего лишь методом, принимающим блок кода в качестве последнего аргумента. Поэтому их можно вызывать и в циклах, и в условиях, и даже составлять подобные конструкции:

it(it("should process +"){(2+3).should == 5}) do
  (3-2).should == 1
end

Оба спека кстати проходят успешно, но страшно даже подумать, где такое можно применить, в отличие от тех же циклов и итераторов. Пример из той же Kaminari:

[User, Admin, GemDefinedModel].each do |model_class|
  context "for #{model_class}" do
    describe '#page' do
      context 'page 1' do
        subject { model_class.page 1 }
          it_should_behave_like 'the first page'
        end
       …
     end
  end
end

Или же пример с условиями:

if Mongoid::VERSION =~ /^3/
  its(:selector) { should == {'salary' => 1} }
else
  its(:selector) { should == {:salary => 1} }
end


7. Макросы


В 2010 году, после введения нового функционала shared examples, Дэвид Челимски заявил, что макросы больше не нужны. Однако если вы все же считаете, что это на��более подходящий способ улучшить код ваших спеков, вы можете создать их примерно так:

module SumMacro
  def it_should_process_sum(s1, s2, result)
    it "should process sum of #{s1} and #{s2}" do
      (s1+s2).should == result
    end
  end
end

describe "sum" do
  extend SumMacro

  it_should_process_sum 2, 3, 5
end

Более подробно останавливаться на этом пункте смысла не вижу, но если вам захочется, то можно почитать [4].

8. Let и Subject


Конструкции let и subject нужны для инициализации исходных значений перед выполнением спеков. Конечно все и так в курсе, что писать так в каждом спеке:
it “should do something” do
  user = User.new
  …
end

совсем не здорово, но обычно все пихают этот код в before:

before :each do
  @user = user.new
end

хотя следовало бы для этого использовать subject. И если раньше subject был исключительно “безымянным”, то теперь его можно использовать и в явном виде, задавая имя определяемой переменной:

describe "number" do
  subject(:number){ 5 }

  it "should eql 5" do
    number.should == 5
  end
end


Let схож с subject’ом, но используется для объявления методов.

Дополнительные ссылки


1. Custom RSpec-2 Matchers
solnic.eu/2011/01/14/custom-rspec-2-matchers.html
2. David Chelimsky — Specifying mixins with shared example groups in RSpec-2
blog.davidchelimsky.net/2010/11/07/specifying-mixins-with-shared-example-groups-in-rspec-2
3. Ben Scheirman — Dry Up Your Rspec Files With Subject & Let Blocks
benscheirman.com/2011/05/dry-up-your-rspec-files-with-subject-let-blocks
4. Ben Mabey — Writing Macros in RSpec
benmabey.com/2008/06/08/writing-macros-in-rspec.html

А в заключение могу только сказать могу только сказать — старайтесь меньше повторяться.