Применение принципа DRY в RSpec

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/160915/

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *