Testing Rails Simple Guide — Part 3

Tim Cheung
Design & Code
Published in
6 min readFeb 24, 2016

--

Honestly, I feel confused when I was first learning the topic “testing in isolation”. I always struggle these questions: “What Stubs, Mocks and Spies means?”, “What purpose of using these techniques?”, “Which techniques should we use to apply certain scenarios?”. In this simple guide, let try to demystify these techniques by using the concrete examples.

Example App for Demonstration

We will continue using the crmdemo app for demonstration. You can follow along this guide to create the test suite, or you can clone this demo app via GitHub repo.

Instantiating Test Data

Remember the best practice “Four Phase Test Pattern” that we learned from part 2 before?

Every test which follows this best practice will consist these four phases:

test do
# setup - Prepare object for this test
# exercise - Execute the functionality we are testing
# verify - Verify the exercise's result against our expectation
# teardown - Resetting all data to pre-test state
end

In the setup phase, we prepare objects for this test. These objects can be catalogized in two types:

System Under Test (SUT) — The unit that we going to test

Collaborators — Is an object that System Under Test (SUT) depend on.

The following show the lead controller with create action:

class LeadsController < ApplicationController
def create
@lead = Lead.new(lead_params)

if @lead.save
redirect_to @lead, notice: 'Lead was successfully created.'
else
render :new
end
end
end

The following example is the controller spec. It asserts that the new template view will be rendered while given the invalid lead data.

require "rails_helper"

RSpec.describe LeadsController do
describe "POST #create" do
context "when lead is invalid" do
it "re-renders the form" do
post :create, lead: attributes_for(:lead, :invalid)

expect(response).to render_template :new
end
end
end
end

The System Under Test (SUT) in this test is “controller” and the Collaborator in this test is “lead”. It may not obvious as you can’t see any setup phase in here.

This controller spec may keep running fine until you change the validation from the collaborator. This test will become unreliable even no controller code has changed.

To address this issue, we can use the test double to replace the collaborators so that our System Under Test (SUT) can purely isolate any collaborators dependency.

Test Doubles

A test double (sometimes referred as “mocks”) is an object which acts as fake collaborators in tests. There are different types of test doubles. The most common types are:

  • Stubs
  • Mocks
  • Spies

Stubs

A stub is an object with no logic. It only returns what you tell it to return.

Here is the example for Stubs:

require "rails_helper"

RSpec.describe LeadsController do
describe "POST #create" do
context "when lead is invalid" do
it "re-renders the form" do
invalid_lead = double(save: false)
allow(Lead).to receive(:new).and_return(invalid_lead)

post :create, lead: { attribute: "value" }

expect(response).to render_template :new
end
end
end
end

Let break it down and explain it one by one:

# Using RSpec to create a double object with save method and return false value. Then assign it to the invalid_lead variable.
invalid_lead = double(save: false)
# Target the Lead collaborator, intercept the new message and return invalid_lead as value.
allow(Lead).to receive(:new).and_return(invalid_lead)
# It sends http post request to create a method with hash parameters.
post :create, lead: { attribute: "value" }
# Verify the response object has rendered new template or not
expect(response).to render_template :new

What scenarios to use Stubs?

When you deal with the collaborators that perform the query methods and have returned data, you can use stubs to isolate these collaborators.

Mocks

A mock is an object that given a specification of messages that should be called, and a test should fail if it’s not called.

Here is the example for Mocks:

require "rails_helper"

RSpec.describe LeadsController do
describe "POST #create" do
context "when lead is valid" do
it "sends email to sale team" do
valid_lead = double(save: true)
allow(Lead).to receive(:new).and_return(valid_lead)
expect(LeadMailer).to receive(:new_lead).with(valid_lead)

post :create, lead: { attribute: "value" }
end
end
end
end

Let break it down and explain it one by one:

# Using RSpec to create a double object with save method and return true value. Then assign it to the valid_lead variable.
valid_lead = double(save: true)
# Target the Lead collaborator, intercept the new message and return valid_lead as value.
allow(Lead).to receive(:new).and_return(valid_lead)
# Target the LeadMailer collaborator, intercept the new_lead message and verify whether the new_lead message has indeed been called with the valid_lead parameter.
expect(LeadMailer).to receive(:new_lead).with(valid_lead)
# It sends http post request to create a method with hash parameters.
post :create, lead: { attribute: "value" }

What scenarios to use Mocks?

When you deal with the collaborators that perform the command methods with no returned data but have side effects, you can use mocks to isolate these collaborators.

Spies

Just like mock, spy also given a specification of messages that should be called, and a test should fail if it’s not called. However, unlike mock which put the expectation in the setup phase, spy follow the four-phase test pattern approach which instantiating test data in the setup phase and put the expectation in the verify phase.

Here is the example for Spies:

require "rails_helper"

RSpec.describe LeadsController do
describe "POST #create" do
context "when lead is valid" do
it "sends email to sale team" do
valid_lead = double(save: true)
allow(Lead).to receive(:new).and_return(valid_lead)
allow(LeadMailer).to receive(:new_lead)

post :create, lead: { attribute: "value" }
expect(LeadMailer).to have_received(:new_lead).with(valid_lead)
end
end
end
end

Let break it down and explain it one by one:

# Using RSpec to create a double object with save method and return true value. Then assign it to the valid_lead variable.
valid_lead = double(save: true)
# Target the Lead collaborator, intercept the new message and return valid_lead as value.
allow(Lead).to receive(:new).and_return(valid_lead)
# Target the LeadMailer collaborator, intercept the new_lead message
allow(LeadMailer).to receive(:new_lead)
# It sends http post request to create a method with hash parameters.
post :create, lead: { attribute: "value" }
# Target the LeadMailer collaborator, verify whether the new_lead message has indeed been called with the valid_lead parameter.
expect(LeadMailer).to have_received(:new_lead).with(valid_lead)

What scenarios to use Spies?

Just like mocks, spies also deal with the collaborators that perform the command method with no returned data but have side effects. If you prefer the four-phase test pattern approach, Spies will be your choice to go with it.

Conclusion

Let recap which test double types should we use:

  • Use stubs when you deal with the collaborators that perform the query methods and have returned data.
  • Use mocks when you deal with the collaborators that perform the command methods with no returned data but have side effects.
  • Use spies when you encountered the same scenarios as mocks but you prefer the four-phase test pattern approach.

Footnote

If you find anything in the above, that is incorrect or have feedback for this guide. Please don’t hesitate and leave your comment below.

If this help, just click ♥ to let more people find it too!

Struggle on building frontend components and integrate to Ruby on Rails? Be sure to check out UiReady — Bootstrap theme marketplaces dedicated for Rails developers.

--

--

Tim Cheung
Design & Code

AWS Certified @CloudWarriorHQ | Join our FB group http://bit.ly/2nMA7Fe for more AWS secrets.