
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
А в заключение могу только сказать могу только сказать — старайтесь меньше повторяться.
