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.

Writing unit tests the right way

We all know what unit tests are and which gems we need to use for unit testing. Unit testing means testing a method in isolation. Unit tests are light weight and execute fast when compared to integration tests. You can write unit and integration tests either using rspec or minitest along with any mocking frameworks. Both frameworks have built-in support for mocking frameworks but in rspec they are extracted as a separate gem called rspec-mocks. Here are some pointers

Tests should help in debugging code.

We write tests to check whether we have covered all scenarios for our code. But we forgot to cross-check whether the test cases helps in debugging when an problem raises. Consider the following scenario

  # app/models/project.rb

  class Project
    has_many :buildings
    has_many :units
    has_many :bookings
  end
  # app/models/building.rb

 class Building
   belongs_to :project
   has_many :units
 end
  # app/models/unit.rb

  class Unit
    belongs_to :building
    has_many :bookings
  end
  # app/models/booking.rb

  class Booking
    belongs_to :unit
    belongs_to :project
    has_many :unit_installments
  end

There are 4 kinds of users called as DSGadmin, Partner, Area Sales manager(ASM), Sales Manger(SM) working under same company(technically called as Distributor in my system) are there. Here is the requirement:

  • DSGadmin can see Partner, ASM, SM bookings and his bookings.
  • Partner can see ASM, SM bookings and his bookings
  • ASM can see see SM and his bookings
  • SM can see his bookings only

Now, to test this I wrote some integration tests, thinking that they will be robust and quick.

   describe "Distributor Report", dont_clean: true do
     context "When DSG admin and partner has done a booking each" do
       before(:all) do
         @today = Date.today.to_s

         @project = create(:project)
         @franchise = create(:franchise)
         @dsg_admin = create(:dsg_admin, resource: @franchise)
         @partner = create(:partner, resource: @franchise, parent: @parent)

         @project.distributors = [@franchise]
         @project.save

         create(:distributor_admin_booking, resource: @franchise, sold_by: @dsg_admin, project: @project.id)
         create(:distributor_staff_booking, resource: @franchise, sold_by: @partner, project: @project.id)
       end 

       it "then DSG admin able to see 2 bookings" do
         login(@dsg_admin)

         post sales_reports_path, search: { project_id: @project.id, start_date: @today, end_date: @today, resource_key: "#{@franchise.id}@@Distributor"}

         expect(assigns[:report].first[1]).to eq(2)
       end 

       it "then partner able to see 1 booking" do
         login(@partner)

         post sales_reports_path, search: { project_id: @project.id, start_date: @today, end_date: @today, resource_key: "#{@franchise.id}@@Distributor"}

         expect(assigns[:report].first[1]).to eq(1)
       end
     end
   end
Distributor Report
  When DSG admin and partner has done a booking each
    then DSG admin able to see 2 bookings
    then partner able to see 1 booking (FAILED - 1)

Failures:

  1) Distributor Report When DSG admin and partner has done a booking each then partner able to see 1 booking
     Failure/Error: expect(assigns[:report].first[1]).to eq(1)
       
       expected: 1
            got: 2
       
       (compared using ==)
     # ./spec/requests/distributor_reports_spec.rb:172:in `block (3 levels) in '

Finished in 2.17 seconds
2 examples, 1 failure

As we can see, when I ran tests then one of my integration tests failed. However, it did not tell me where things failed. Now I realized the need to write some good unit tests and got down to it.

  describe "#b_match" do
    context "should return criteria as" do
      before do
        @sales.stub(:c_project).and_return(nil)
        @sales.stub(:resource).and_return(nil)
        @sales.stub(:default_match).and_return({'deleted_at' => nil})
      end 

      it "returns {'deleted_at' => nil} if no project or resource has selected" do
        @sales.b_match
        expect(@sales).to have_received(:default_match)
      end 

      it "returns {{'deleted_at' => nil, sold_by_id: {'$in' => @user.assigned_users_ids}} if selected resource is distributor" do
        expect(@sales.b_match).to eq('$match' => {'deleted_at' => nil, sold_by_id: {'$in' => @partner.assigned_users_ids}})
      end
    end
  end
  Sales 
    #b_match 
      returns criteria as {{'deleted_at' => nil, sold_by_id: {'$in' => @user.assigned_users_ids}} if selected resource is distributor


Failures:

  1) Sales#b_match should return criteria as returns {{'deleted_at' => nil, sold_by_id: {'$in' => @user.assigned_users_ids}} if selected resource is distributor
     Failure/Error: expect(@sales.b_match).to eq('$match' => {'deleted_at' => nil, sold_by_id: {'$in' => @partner.assigned_users_ids}})
       
       expected: {"$match"=>{"deleted_at"=>nil, :sold_by_id=>{"$in"=>["5378c281472d208bc100000d"]}}}
            got: {"$match"=>{"deleted_at"=>nil}}
       
       (compared using ==)
       
       Diff:
       @@ -1,2 +1,2 @@
       -"$match" => {"deleted_at"=>nil, :sold_by_id=>{"$in"=>["5378c281472d208bc100000d"]}}
       +"$match" => {"deleted_at"=>nil}
       
     # ./spec/reports/sales_spec.rb:75:in `block (4 levels) in '

Finished in 0.45649 seconds
9 examples, 1 failure

Now, with this failure, I found the issue in b_match method. In the fix below, it was obvious that the I required the `merge!` instead of the `merge` method. However, the integration tests never pointed to this particular failure – the unit test did!

  def b_match
     match = default_match
     if @current_resource.class == Distributor
+      match.merge!(sold_by_id: {'$in' => @user.assigned_users_ids})
-      match.merge(sold_by_id: {'$in' => @user.assigned_users_ids})
     end
     {'$match' => match}
   end

Unit tests should be independent

Unit tests should be less tightly coupled to avoid dependency. Consider the scenario where we need to calculate the compound interest if a customer didn’t pay their instalment on time. Here is a code snippet

  # app/models/unit_installment.rb

  class UnitInstallment
    field :percentage
    field :amount, type: Float, default: 0
    field :order, type: Integer
    field :received_amount, type: Float, default: 0
    field :interest_amount, type: Integer, default: 0
    field :is_due, type: Boolean, default: false

    belongs_to :booking

    before_create :initialize_is_due

    def initialize_is_due
      self.is_due = unit.building.building_schemes.where(payment_schedule_id: payment_schedule_id, scheme_id: scheme_id).first.try(:due)
      make_due
    end
  end

Now I have written the code to calculate the interest for a installment amount. Here is my test case to check interest calculation:

require 'spec_helper'

describe InterestAmount do
  before do
    UnitInstallment.any_instance.stub(:initialize_is_due).and_return(true)
    UnitInstallment.any_instance.stub(:update_booking).and_return(true)
    UnitInstallment.any_instance.stub(:valid?).and_return(true)
  end 

  let!(:booking) { create(:org_booking) }
  let!(:installment) { create(:unit_installment, booking: booking) }
  let!(:project) { create(:project) }

  context "#calculate" do
    it "should update interest amount on installment" do
      interest = InterestAmount.new(installment: installment, project: project, booking: booking)
      interest.stub_chain(:interest, :round).and_return(100)
      interest.calculate

      expect(installment.reload.interest_amount).to eq(100)
      expect(interest).to have_received(:interest)
    end
  end

Notice that, I have stubbed before_create callback which deals with unit, payment_schedule, scheme objects. So if either unit, payment_schedule or scheme are invalid then I can’t create unit installment object which eventually can’t check interest calculation.

I hope it helps you understanding how to write unit tests that help in debugging.

Payment gateway testing using webmock

Testing the payment gateway involves lots of scenarios like what should happen if exception is raised while doing payment. To test this scenario we have to call payment gateway API and take response. But herein lies a problem. The test code you have written may work sometime and won’t work another time. The reason behind this failure is, whenever you call payment gateway in quick succession, then gateway gives error. So, we can’t test payment scenarios by calling actual gateway API. How can we then test gateway integration? The answer is stubbing the gateway requests using webmock.

Testing recurring payment with authorize.net

  1. For configure ‘webmock’ for test environment and do bundle install.
  2. Executes your controller specs. Anyway, specs will fail as we disable all real HTTP requests.
  3. Download the recurring payment response from authorize.net using curl like below:
curl -H 'Content-Type:text/xml' <?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ARBCreateSubscriptionRequest xmlns=\"AnetApi/xml/v1/schema/AnetApiSchema.xsd\">\n  <merchantAuthentication>\n    <name>2ErYn5tb5X</name>\n    <transactionKey>75mhe6B5TLNwA47b</transactionKey>\n  </merchantAuthentication>\n  <subscription>\n    <paymentSchedule>\n      <interval>\n        <length>1</length>\n        <unit>months</unit>\n      </interval>\n      <startDate>2011-09-14</startDate>\n      <totalOccurrences>1</totalOccurrences>\n    </paymentSchedule>\n    <amount>10.00</amount>\n    <payment>\n      <creditCard>\n        <cardNumber>370000000000002</cardNumber>\n        <expirationDate>2011-21</expirationDate>\n      </creditCard>\n    </payment>\n    <billTo>\n      <firstName>Test</firstName>\n      <lastName>Account</lastName>\n      <company></company>\n      <address></address>\n      <city></city>\n      <state></state>\n      <zip></zip>\n      <country></country>\n    </billTo>\n  </subscription>\n</ARBCreateSubscriptionRequest>\n" https://apitest.authorize.net/xml/v1/request.api > success_response

This sends an XMLHttpPost request to https://apitest.authorize.net/xml/v1/request.api and copies the response to success_reponse file.

We now have response in hand. Place this response in spec/support/webmock/authorize.net directory. Now, we need to configure the response such a way that whenever we send recurring payment request it should return our downloaded response.

Create a file called payment.rb in spec/support directory.

    #spec/support/payment.rb
    require 'webmock/rspec'

    RSpec.configure do |config|
     VALID_CARD  = 370000000000002
     FIRST_NAME   = "test"
     LAST_NAME    = "account"
     EXPIRY_YEAR  = (Time.now.year + 10).to_s
     EXPIRY_MONTH = (Time.now.month + 1).to_s
     CVV = "123"

     config.fixture_path = "#{::Rails.root}/spec/fixtures"
     # configure test response for purchase and recurring payment
     authorize_net = config.fixture_path + '/webmock/authorize.net/'
     payment_responses = []
     Dir["#{authorize_net}/**/*"].each do |path|
       payment_responses << path
     end
     payment_gateway = "https://apitest.authorize.net/xml/v1/request.api"
     success_params = []

     # configure recurring payment request parameters with different values
     10.times do |i|
       amount = 5 * i

       # Stubbing request parameters with different values
       success_params << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ARBCreateSubscriptionRequest xmlns=\"AnetApi/xml/v1/schema/AnetApiSchema.xsd\">\n  <merchantAuthentication>\n    <name>2ErYn5tb5X</name>\n    <transactionKey>75mhe6B5TLNwA47b</transactionKey>\    n  </merchantAuthentication>\n  <subscription>\n    <paymentSchedule>\n      <interval>\n        <length>1</length>\n        <unit>months</unit>\n      </interval>\n      <startDate>#{(Date.today + 1.month).to_s(:db)}</startDate>\n      <totalOccurrences>1</to    talOccurrences>\n    </paymentSchedule>\n    <amount>#{amount}.00</amount>\n    <payment>\n      <creditCard>\n        <cardNumber>#{VALID_CARD}</cardNumber>\n        <expirationDate>#{EXPIRY_YEAR}-#{EXPIRY_MONTH}</expirationDate>\n      </creditCard>\n    </payment>\n    <billTo>\n      <firstName>#{FIRST_NAME}</firstName>\n      <lastName>#{LAST_NAME}</lastName>\n      <company></company>\n      <address></address>\n      <city></city>\n      <state></state>\n      <zip></zip>\n      <country></country>\n    </billTo>\n  </subscription>\n</ARBCreateSubscriptionRequest>\n"

       config.around(:each) do |example|
         success_params.each { |parameter| WebMock::API.stub_request(:post, payment_gateway).
                with(:body => parameter,
                     :headers => {'Accept'=>'*/*', 'Content-Type'=>'text/xml'}).
                to_return(:status => 200,
                          :body => File.new(payment_responses[0]), :headers => {})}
       end
     end
    end

    WebMock.disable_net_connect!(:allow_localhost => true)
    example.call
    WebMock.allow_net_connect!

I think you understand how to stub recurring payment requests. Now we will see another example.

Purchase using authorize.net

As usual download purchase response from authorize.net like below:

   curl -H "Content-Type:application/x-www-form-urlencoded" -d login=2ErYn5tb5X -d key=75mhe6B5TLNwA47b -d key=x_type=AUTH_CAPTURE -d x_encap_char=%24 -d x_first_name=test -d x_invoice_num= -d x_last_name=account -d x_tran_key=75mhe6B5TLNwA47b -d x_card_num=370000000000002 -d x_version=3.1 -d x_relay_response=FALSE -d x_exp_date=1021 -d x_login=2ErYn5tb5X -d x_delim_data=TRUE -d x_description= -d x_card_code=123 -d x_amount=30.00 -d x_test_request=FALSE -d x_delim_char=%2C https://test.authorize.net/gateway/transact.dll > purchase
 

It will send as HTTP Post request to https://test.authorize.net/gateway/transact.dll and copy the response into purchase file. Place this file in spec/fixtures/webmock/authorize.net directory.

Now stub the purchase request:

   #spec/support/payment.rb
   stub_request(:post, "https://test.authorize.net/gateway/transact.dll")
        with(:body => /^[A-Za-z&_0-9#%.=]*$/,
             :headers => {'Accept'=>'*/*', 'Content-Type'=>'application/x-www-form-urlencoded'}).
        to_return(:status => 200, :body => File.new(payment_responses[1]), :headers => {})

Note: We can’t stub request with different parameters as it is a HTTP Post request. So match request parameters against regular expression.

Hope this helps! Please let me know if you see any complex payment scenarios. I will give a shot!.