📜 ⬆️ ⬇️

Applying DRY principle in RSpec



DRY (Don't Repeat Yourself) is one of the cornerstones of modern development, and especially among ruby ​​programmers. But if, when writing ordinary code, repeating fragments are usually easily grouped into methods or individual modules, then when writing tests, where repeated code is sometimes even more, this is not always easy to do. This article contains a small overview of the means to solve such problems when using the RSD BDD framework.

1. Shared Examples


The most famous and frequently used method for creating reusable code for Rspec. Great for testing class inheritance and module inclusion.

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 

In addition, Shared Example Groups have some additional functionality, which makes them much more flexible to use: passing parameters, passing blocks and using let in the parent group to define methods.
')
 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 

For more information on where and how certain methods will be available, see David Chelimsky [2].

2. Shared Contexts


This feature is somewhat unknown due to its relative novelty (appeared in RSpec 2.6) and its narrow scope. The most suitable situation for using shared contexts is the presence of several specs, which require the same initial values ​​or final actions, usually specified in the before and after blocks. Documentation hints at this:

 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 

A very convenient thing in shared_context is the possibility of including them according to the meta information specified in the describe block:

 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. Factory facilities


Another simple, but very important point.

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

Instead of repeatedly writing similar constructions, you should use the factory_girl gem or its analogs. The advantages are obvious: the amount of code is reduced and there is no need to rewrite all the specs if you decide to change status to status_code.

4. Own matchrs


The ability to define your own matchers is one of the coolest opportunities in RSpec, thanks to which it is unrealistic to increase the readability and elegance of your specs. Just an example.
Before:
 it “should make user cool” do make_cool(user) user.coolness.should > 100 user.rating.should > 10 user.cool_things.count.should == 1 end 

After:
 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 

Agree, it has become much better.
RSpec allows you to set error messages for your own matchrs, display descriptions and perform cheating, which makes matchers so flexible that they are no different from built-in ones. To realize all their power, I offer the following example [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. One-liners


RSpec provides the ability to use single-line syntax when writing simple specs.

An example from a real opensource project ( 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 

Clearly much better than:
 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 1do @page.current_page.should == 1 end …. #etc 


6. Dynamically created specs


The key point here is that the it construct (as well as context and describe) is just a method that takes a block of code as the last argument. Therefore, they can be called both in cycles and under conditions, and even make up similar constructions:

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

By the way, both specs are successful, but it's scary to even think where this can be applied, unlike the same cycles and iterators. An example from the same 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' endend end end 

Or an example with conditions:

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


7. Macros


In 2010, after the introduction of the new functionality of shared examples, David Chelimsky said that macros are no longer needed. However, if you still think that this is the most appropriate way to improve the code of your specs, you can create them like this:

 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 

I don’t see any sense in dwelling on this point, but if you want, you can read [4].

8. Let and Subject


The let and subject constructs are needed to initialize the initial values ​​before performing the specs. Of course, everything is already in the know that to write so in every spec:
 it “should do something” do user = User.new … end 

It's not cool at all, but usually everyone shoves this code in before:

 before :each do @user = user.new end 

although subject should be used for this. And if before the subject was exclusively “nameless”, now it can also be used explicitly by specifying the name of the variable being defined:

 describe "number" do subject(:number){ 5 } it "should eql 5" do number.should == 5 end end 


Let is similar to subject, but it is used to declare methods.

Additional links


1. Custom RSpec-2 Matchers
solnic.eu/2011/01/14/custom-rspec-2-matchers.html
2. David Chelimsky - 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

And in conclusion I can only say, I can only say - try to repeat less.

Source: https://habr.com/ru/post/160915/


All Articles