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

в 18:18, , рубрики: DRY, rspec, ruby, метки: , ,

Применение принципа 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

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

Автор: rsludge

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js