What makes rspec3 so interesting?

Being an rspec fan I have been waiting for quite some time for the rspec3 final release. rspec3 has finally been released and it’s ready for production use. There are many interesting changes that has been incorporated in rspec3. Thanks to myron, Andy and david and other contributors. Here are the few changes that makes testing more fun:

Changes in rspec-expectations:

Compound Expectations: Composing a expectation using two or more expectations is called compound expectation. Here is the example:

  # In rspec2
  describe String do
    it "expect to be a instance of String and it should be equal to 'RUBY IS AWESOME'"
      string = "RUBY IS AWESOME"

      expect(string).to be_a(String)
      expect(string).to eql("RUBY IS AWESOME")
    end
  end
  # In rspec3
  RSpec.describe String do
    example "expect to be a instance of String and it should be equal to 'RUBY IS AWESOME'"
      string = "RUBY IS AWESOME"

      expect(string).to be_a(String).and eql("RUBY IS AWESOME")
    end
  end

Note: Not only combined pre-defined matchers you can also combined custom matchers also. Below is the example:

  RSpec::Matchers.define :be_a_first do |expected|
    match do |actual|
      actual.first == expected
    end
  end

  RSpec::Matchers.define :be_a_last do |expected|
    match do |actual|
      actual.last == expected
    end
  end

  RSpec.describe Array do
    describe "#sort" do
      example "expect 'AWESOME' be a first and 'RUBY' at last elements respectively" do
        expect("RUBY IS AWESOME".split(" ")).to be_a_first("AWESOME").and be_a_last("RUBY")
      end
    end
  end

Composing Expectation: Composing a expectation with pre-defined expectations or custom expectations so that you can express exactly what you want. Below is the example:

  RSpec.describe Array do
    describe "#sort" do
      example "expect 'AWESOME' be a first and 'RUBY' at last elements respectively" do
        expect("RUBY IS AWESOME".split(" ").sort).to match(be_a_first("AWESOME")).and match(be_a_last("RUBY"))
      end
    end
  end

Changes in rspec-mocks: The following changes would be interesting if you use mocks in your test suite.

Stub chain of methods: Once in a while you might face an issue how to stub chain of methods? In rspec2 you have done as follows:

  describe "String" do
    it "able to call upcase.split.reverse on a string object" do
      string = "ruby is awesome"

      allow(string).to receive(:upcase).and_return(upcase = "RUBY IS AWESOME")
      allow(upcase).to receive(:split).and_return(string_split = ["RUBY", "IS", "AWESOME"])
      allow(string_split).to receive(:reverse).and_return(["AWESOME", "IS", "RUBY"])

      expect(string.upcase.split(" ").reverse).to eql(["AWESOME", "IS", "RUBY"])
    end
  end

While in rspec3 you can do

   describe "String" do
    it "able to call upcase.split.reverse on a string object" do
      string = "ruby is awesome"

      allow(string).to receive_message_chain(:upcase, :split, :reverse).and_return(["AWESOME", "IS", "RUBY"])
      expect(string.upcase.split(" ").reverse).to eql(["AWESOME", "IS", "RUBY"])
    end
  end

Scopes:
Till now you couldn’t stub methods in before(:all) scope. Although you can do in rspec3, rspec team suggested not to use all the times. Here is the example how to use:

   RSpec.describe String do
      before(:context) do
        RSpec::Mocks.with_temporary_scope do
           string = "ruby is awesome"
           allow(string).to receive(:upcase).and_return("RUBY IS AWESOME")
           @hash = string.upcase
        end
      end 

      example "able to split the string" do
        expect(@hash.split(" ")).to eq(["RUBY", "IS", "AWESOME"])
      end 

      example "able to split and reverse" do
        expect(@hash.split(" ").reverse).to eq(["AWESOME", "IS", "RUBY"])
      end
    end

Changes in rspec-rails

In rspec-rails they have changed the structure a little. From rspec3 onwards, rspec team segregated core rspec configuration from rspec-rails configuration. All core rspec configuration like expectations.syntax = :expect, profile_examples = 10 moved to spec_helper and all rspec-rails configuration like use_transactional_fixtures moved to rails_helper. verify partial doubles: Prior to verifying partial double let see what is partial double. Partial double is a real object which acts as a test double in testing. Using verify_partial_doubles option you can cross check that by unknowingly you mock a method which is not there in the class. Here is the example:

  RSpec.describe Array do
    describe "#to_hash" do
      example "convert array to hash" do
        a = [1,2,3]

        allow(a).to receive(:to_hash).and_return({"1" => 0, "2" => 0, "3" => 0})
        expect(a.to_hash).to eql({"1" => 0, "2" => 0, "3" => 0})
       end
     end
   end

   # spec/spec_helper

   config.mock_with :rspec do |mocks|
     mocks.verify_partial_doubles = true
   end

Here is output when I run the specifications.

Failures:

  1) Array#to_hash convert array to hash
     Failure/Error: allow(a).to receive(:to_hash).and_return({"1" => 0, "2" => 0, "3" => 0})
       [1, 2, 3] does not implement: to_hash
     # ./spec/models/person_spec.rb:63:in `block (3 levels) in '

There is no method called to_hash exists on Array class. But I mock that method on that partial double. As method is not defined and I set verify_partial_doubles to true the above spec has failed. Till now we have seen changes in rspec-rails, rspec-mocks and rspec-expectations. Now we will see changes in rspec-core.

If you have observed carefully, I have not used 'it' throughout the post. In rspec3, it is suggested not to use 'it' to define a test case. Instead use 'example' to define a test case as 'it' might deprecated on future versions. Not just 'it', but a few other aliases have been added that make the test cases more readable. In rspec3, before(:example) is now an alias for before(:each) and before(:context) is an alias of before(:all). Not only can you use pre-defined aliases but also define your own aliases. Here is the example:

   RSpec.configure do |config|
     config.alias_example_to :frun, run: true
   end

Now you can use frun to define a test case with metadata as run:true. Here is the example:

   frun "should adds 'run:true' in example metadata" do
     expect(1+1).to eq(2)
   end

Now you can run the test case using run tag as follows:

  $rspec spec/models/person_spec.rb -t run
   Run options: include {:run=>true}
   .

   Finished in 0.02857 seconds (files took 1.1 seconds to load)
   1 example, 0 failures

Apart from few additions there are few things has been removed from rspec3. From no onwards you can’t do the following:

   RSpec.describe "Array" do
     subject { [1,2,3] }

     its(:length) { is_expected.to eq(3)}

     example "contains three items" do
       expect([1,2,3]).to have_at_least(3).items
     end
   end

To make above example work you have to include rspec-its and rspec-collection_matchers gems in your Gemfile. I hope these notable changes in rspec3 help you. Please help me expand this post by commenting on things I may have missed.