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