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.

Handy gems for minitest-rails part2

Mocks and stubs are not new concepts that have been introduced in minitest. If you want to get detailed understanding about what are mocks and stubs you should read mocks aren’t stubs written by Martin Fowler. To achieve mocking and stubbing in minitest-rails you don’t need to include any separate gem like rspec-mocks in rspec. Let see how to do work with mocks and stubs in minitest then check what are gems available to add extra functionality.

Method stubbing
Stubbing a method means, set a pre-defined return value of a method in test-case. Let’s check how to do method stubbing in minitest:

  # app/models/user.rb
  class User < ActiveRecord::Base
    def foo
      "Executes foo"
    end

    def bar
      "Executes bar"
    end

    def new_method
      foo
      bar
    end
  end
  it "stubbing known method" do
    user = User.new
    user.stub(:foo, "Return from test foo") do
      user.new_method.must_equal "Executes bar"
    end
  end

In this example, I stub foo and check what would be the output of new_method. As matter of fact it won’t call actual foo method instead it calls stub foo method. Now that we have  seen how to stub and instance method, let check how to stub on any instance.

minitest stub any instance:

Using this gem you can stub a method on any instance of a class. The below example demonstrates how to do that:

  # app/models/user.rb
  class User < ActiveRecord::Base
    def foo
      "Executes foo"
    end

    def bar
      "Executes bar"
    end

    def new_method
      foo
      bar
    end
  end
  it "stubbing on any instance" do
    User.stub_any_instance(:foo, "Return from test foo") do
      p User.new.foo
      User.new.foo.must_equal "Return from test foo"
    end
    p User.new.foo
  end

Output:
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
Run options: –seed 36985

# Running tests:

“Return from test foo”
“Executes foo”

Notice that the stub method on any instance of User is valid only in the stub_any_instance block.

minitest-stub-const:

As name suggests you can stub constants of a class. Here is the example:

  it "stubbing a constant" do
    User.stub_const(:CONSTANT, "Test constant") do
      User::CONSTANT.must_equal "Test constant"
    end
  end

Here CONSTANT is a constant defined on User class. Using minitest-stub-const we can not only stub constants but also class methods. Here is the example:

  # app/models/user.rb
  def self.add
    "User created on development"
  end

  def add_user
    User.add
  end
  it "stub a class method" do
    m = MiniTest::Mock.new
    m.expect(:add, "Created user from testing")

    User.stub_const(:User, m) do
      User.new.add_user.must_equal "Created user from testing"
    end

    m.verify
  end

In above example, I stub User with mock object m. Now whenever User is referred, it is replaced with the mock object m. Now, we call the method add on the Mock object but this method is already mocked using m.expect(:add, "Created user from testing").

Mocking:

Mocking an object is one of the ways to do unit testing. Mock objects are the objects that we use in a test environment instead of real objects. We can create mocks using double in rspec to create dummy objects, where as in minitest you can do as Minitest::Mock.new which creates mock object. On mock object we can set expected messages. Here is an example:

  it "mocking a method" do
    user = Minitest::Mock.new
    user.expect(:another_method, "from test bar")

    user.another_method.must_equal "from test bar"

    user.verify
  end

You can also mock objects using minitest firemock as follows:

  it "mocking a method" do
    user = Minitest::FireMock.new("User")
    user.expect(:another_method, "from test bar")

    user.another_method.must_equal "from test bar"

    user.verify
  end

The only difference between Minitest::Mock and Minitest::FireMock is, when you try to mock undefined method then Minitest::FireMock raises an exception whereas Minitest::Mock won’t.

Hope this helps while you writing mocks and stubs in minitest. Any suggestions, queries would be welcome.

Handy gems for minitest-rails

Welcome to ‘minitest’ world! Minitest is yet another ruby testing framework but it’s not a replacement to rspec. In rspec you can readily use things like ‘subject’, ‘metadata’ etc. However, minitest, as the name suggests, is a light-weight testing framework and does not include a lot of these features automatically. Here is a list of gems that you can use to enhance minitest-rails.

1. m

m is a test runner similar to rspec command in rspec framework. Using this we can run single test using line numbers too. Users who don’t like this gem can also run tests using default rake tasks provided by minitest.

2. minitest-metadata

metadata is one of the cool features in rspec. Using this we can tag scenarios across files. Unfortunately minitest doesn’t support this feature by default. We have to include a separate gem called minitest-metadata. We can implement rspec conditional hooks functionality through this gem as follows:

  require "test_helper"

  describe UsersController do

    before do
      @user = create(:user) if metadata[:before_each] == true
    end

    it "must create user" do
      assert_difference('User.count') do
        post :create, user: attributes_for(:user)
      end

      assert_redirected_to user_path(assigns(:user))
    end

    it "must show user", before_each: true do
      get :show, id: @user
      assert_response :success
    end
  end

When we run the file before code block will run only for the latter test case.

3. minitest-implicit-subject

subject is one of the best ways to dry up your specs. To dry up your specs in minitest you have explicity include a gem called minitest-implicit-subject. Here is an example:

  require "test_helper"

  describe User do
    it "foo returns bar" do
      subject.must_equal User
    end
  end

There is slight difference that I have observed in minitest subject from rspec subject. In minitest if you write subject it returns current class name under which it has included. In this scenario it returns output as User class not User object. But in rspec it returns User object.

4. minitest-spec-expect

rspec expectation framework lets you code example more readable. In minitest there is separate gem called minitest-spec-expect available to make your code example readable as same as in rspec. Here is an example:

  require "test_helper"

  describe User do
    it "must be valid" do
      expect(subject.new.valid?).to_equal true
    end
  end

5. minitest-rails-capybara

Using capybara is one of the ways to implement your integration test suite in rspec. We need to include separate gem called minitest-rails-capybara in minitest to write integration tests using the capybara DSL. Here is a simple example:

   require "test_helper"

   feature "User" do
     scenario "should be able save" do
       visit users_path
       click_link 'New User'
       fill_in "user_first_name", with: 'Siva'
       click_button 'Create User'

       expect(User.all.count).to_equal 1
     end
   end

In my next post I will talk about the gems that allow us to mock and stub requests in minitest.