- Maximizing coverage with the fewest tests possible
- Testing everything once
- Testing the interface and not the implementation
- How to mock reliably
Slow
Fragile
Expensive
Thorough
Stable
Fast
Few
Test Incoming Query Messages
Test incoming query messages by making assertions about what they send back
Incoming Command Messages
Test incoming command messages by making assertions about direct public side effects
Receiver has sole responsiblity for asserting the result of direct public side effects
Messages sent to self
Messages sent to self
Outgoing Query Messages
class Application
attr_reader :candidate
def initialize(candidate)
@candidate = candidate
end
def candidate_name
"Candidate : " + @candidate.full_name
end
end
describe "#candidate_name" do
it "returns candidates full name" do
candidate = Candidate.new("Serge Backflip")
application = Application.new(candidate)
expect(application.candidate_name).
to eq "Candidate : Serge Backflip"
end
end
describe "#candidate_name" do
it "returns candidates full name" do
candidate = Candidate.new("Serge Backflip")
application = Application.new(candidate)
expect(application.candidate_name).
to eq "Candidate : Serge Backflip"
# REDUNDANT
expect(application.candidate.full_name).
to eq "Serge Backflip"
end
end
describe "#candidate_name" do
it "returns candidates full name" do
candidate = Candidate.new("Serge Backflip")
application = Application.new(candidate)
# STILL REDUNDANT
expect(candidate).to receive(:full_name)
expect(application.candidate_name).
to eq "Candidate : Serge Backflip"
end
end
- Over Specification - Adds cost with no benefits
- Binds you to the implementation details of the full_name method
- If a message has no visible public side effects, the sender should not test it. That's the responsibility of the receiving class
Outgoing Command Messages
class Department
def initialize(name)
@name = name
end
def update_name(new_name)
old_name = name
self.name = new_name
self.save!
ChangeLog.create_entry(self, old_name, new_name)
end
end
it "updates name and create a changelog entry" do
department = Department.new("Web Development")
expect { department.update_name("Engineering") }.
to change { department.name }.
from("Web Development").
to("Engineering")
# make an assertion about the db for ChangeLog?
end
- Is this Departments responsibility?
Outgoing Command Messages
it "updates name and create a changelog entry" do
department = Department.new("Web Development")
# this message MUST get sent
expect(ChangeLog).to receive(:create_entry).
with(department, "Web Developent", "Engineering")
expect { department.update_name("Engineering") }.
to change { department.name }.
from("Web Development").
to("Engineering")
end
- Department IS responsible for sending create_entry to the receiver
- The public api DEPENDS on the create_entry message being sent
- Expect to send outgoing command messages
Mocks - assertion that a message is received.
=> Returns nil by default
expect(User).to receive(:full_name)
Stub - No assertion. When this message is received
=> provides a specific return.
allow(User).to receive(:full_name).and_return('first last')
Mock and Stub - Assertion that a message is received
=> provides a specific return.
expect(User).to receive(:full_name).and_return('first last')
Partial Doubles & verifying partial doubles
class Department
def update_name(new_name)
old_name = name
self.name = new_name
self.save!
ChangeLog.create_entry(self, old_name, new_name)
end
end
# department_spec.rb
it "updates name and create a changelog entry" do
department = create(:department)
allow(ChangeLog).to receive(:create_entry).
and_return(true)
end
# spec_helper.rb
RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
- Protects against api changes
- Protects against typos
Mocking External Dependencies
class UserEmailer
def initialize(user)
@user = user
end
def email
send_email_to(user.email)
end
end
### Factory Girl
it "sends email to user" do
user = FactoryGirl.create(:user, :email => "serge.backflip@example.com" )
user_emailer = UserEmailer.new(user)
expect(user_emailer.email).to deliver_email_to(
"serge.backflip@example.com"
)
end
### Test Double
it "sends email to user" do
user = double("user")
allow(user).to receive(:email).
and_return("serge.backflip@example.com")
user_emailer = UserEmailer.new(user)
expect(user_emailer.email).to deliver_email_to(
"serge.backflip@example.com"
)
end
# mocking external dependencies makes the test faster.
# testing user, is *not* the responsiblity of UserEmailer.
# all we need is an object that responds to email
### Instance Double
it "sends email to user" do
user = instance_double("User")
allow(user).to receive(:email).and_return("serge.backflip@example.com")
user_emailer = UserEmailer.new(user)
expect(user_emailer.email).to deliver_email_to(
"serge.backflip@example.com"
)
end
# if spec fails
# output: "User does not implement: email"
- Protects against api changes
- class_double works the same way for class methods
Mocking external dependencies with Distant Side Effects
class ApplicationController
def accept
# ...
application.hire(application_details)
end
end
it "accepts application" do
# ...
expect(application).to receive(:hire).and_call_original
end
Protects against distant side effects that may error out
- Test everything once
- Test the public interface and not the implementation
- Prefer test doubles over factory girl or actual objects
- Prefer instance_double and class_double over doubles
- Understand these "rules" but use your best judgement for more complex test cases
- Controllers and dependency management
- Understanding waiting behavior to avoid finicky capybara specs
- Making selenium/feature specs readable, reliable, and performant