Simplify testing with mocks and stubs
@h6165
Abhishek Yadav
ரூபீ ப்ரோக்ராமர்
Co-organizer: Chennai.rb
The background
Previous talk: Getting started with testing using rspec:
https://speakerdeck.com/h6165/lets-start-testing
Covers the premise and fundamentals.
The need
- Simulating difficult behavior
 - Simplifying dependent objects
 
=> Make testing simpler, easier and fun
Simulating difficult behavior
Sometimes we have code
whose actual execution in the tests is costly/difficult
but not important to the outcomes of the test
Simulating difficult behavior
# Method under test: 
# (within User model)
def complete_registration
  self.state = 'registered'
  self.save
  UserMailer.registered(self).deliver!
end
# In this test case, we are only checking the value of state
# We can have a separate test for email delivery,
# but in this one, we are not concerned with the email part
it 'should mark the state as registered' do
  user.complete_registration
  expect(user.state).to eq('registered')
end
    Simulating difficult behavior
Sometimes it is more complicated than a simple email:
- A complex query involving several models
 - A slow step (like external API calls)
 - A cost ($) sensitive step (API call involving a paid service )
 
Simplifying dependent objects
Our code could be dependant on objects of other types (class/model).
While writing tests, we have to make sure these objects are created and configured correct to have the desired outcome
Simplifying dependent objects
# Summary of comments, as they show on blog sidebars
# The summary contains the user's name,
# and the first twenty characters of the comment text
#
# Within Comment model, 
#   Comment has a column 'body' and
#   Comment belongs_to :user
#
def comment_summary
  user.name + "-" +
  self.body[0..20]
end
# The method is dependent on the user object.
# While writing tests, we will need to create the user object too
# This may not seem like a big deal here,
# but when there are too many objects,
# or when object creation is complicated, it becomes important
    Definitions
| Test double | Object that stands for the real object. Much like 'stunt double'. | 
| Stub | Object with a method overridden to return fake value | 
| Mock | Object with a method overridden to return fake value, and the method must be called | 
| Spy | Test double + testable expectation later | 
Note:
- Definitions are not standardised, so this as a general idea at best
 - There are partial doubles and full doubles too. Partial double is more like stubs here.
 
Libraries
- rspec-mocks
 - mocha
 - rr
 - flexmock
 
Most popular: rspec-mocks, mocha
Ref: https://www.ruby-toolbox.com/categories/mocking
Stub example
# Method under test: (extracted email sending to separate method)
def send_email
  UserMailer.registered(self).deliver!
end
def complete_registration
  self.state = 'registered'
  self.save
  send_email
end
# Stubbing the email step
it 'should mark the state as registered' do
  allow(user).to receive(:send_email)
  user.complete_registration
  expect(user.state).to eq('registered')
endMock example
# Method under test: (extracted email sending to separate method)
def send_email
  UserMailer.registered(self).deliver!
end
def complete_registration
  self.state = 'registered'
  self.save
  send_email
end
# We want to make sure 'send_email' is invoked,
# even if email is not actually sent
# This test fails if we remove the 'send_email' call from above code
it 'should try sending the email' do
  expect(user).to receive(:send_email)
  user.complete_registration
endSpy example
# Method under test: (extracted email sending to separate method)
def send_email
  UserMailer.registered(self).deliver!
end
def complete_registration
  self.state = 'registered'
  self.save
  send_email
end
# We want to make sure 'send_email' is invoked,
# even if email is not actually sent
# This test fails if we remove the 'send_email' call from above code
it 'should try sending the email' do
  allow(user).to receive(:send_email)
  user.complete_registration
  expect(user).to have_received(:send_email)
endDouble example
# Within Comment model, 
#   Comment has a column 'body' and
#   Comment belongs_to :user
def comment_summary
  user.name + "-" +
  self.body[0..20]
end
## Test
it "should be *Abhishek-This is the best*" do
  # Instead of creating a real User object,
  # which may be difficult, we create a double with just the name
  user = double(name: 'Abhishek')
  comment = Comment.new(body: 'This is the best')
  expect(comment.comment_summary).to eq('Abhishek-This is the best')
end
    Test complex code with stubs
- Starting tests on legacy code is difficult. Without tests/TDD, methods tend to get longer, with multiple responsibilities and dependencies entangled.
 - We try testing such methods one behaviour at a time. But while doing so, data relevant to all other parts must be created. And behaviour for other parts be satisfied
 - That makes writing tests difficult and slow.
 - Stubbing helps approaching such a codebase. The idea is: stub all the data/behaviour that is not relevant to the current test-case. Test various parts as separate test-cases
 
Test complex code with stubs: example
class Partner < ActiveRecord::Base
  ## Create Location record, geocode it, and associate it
  def update_locations(country, state, city)
    if !country.blank?
      
      l = Location.where(city: city, state: state, country: country).last
      l = Location.new(city: city, state: state, country: country)
      if l.latitude.nil? || l.longitude.nil?
        lat, lng = Geocoder.coordinates("#{country}, #{state}, #{city}")
        if lat && lng
          l.latitude = lat
          l.longitude = lng
        end
      end
      l.save!
      # Associate the Location with partner
      pl = PartnerLocation.where(partner_id: self.id, location_id: l.id).last
      if pl.nil?
        pl = PartnerLocation.new(partner_id: self.id, location_id: l.id)
        pl.save!
      end
    end
  end
end
    Test complex code with stubs: example
# Test case: 1
# Location does not exist, Geocoding is possible
it "test-1" do
  partner = Partner.new
  allow(partner).to receive(:id).and_return(1)
  allow(Geocoder).to receive(:coordinates).and_return([13, 83])
  partner.update_locations('India', 'Tamil Nadu', 'Chennai')
  location = Location.last
  expect([location.latitude, location.longitude]).to eq([13, 83])
end
    Here we stub the Geocoder to return fake values we can use.
We also stub the partner#id to avoid saving it (it is used in the last part of the code)
Test complex code with stubs: example
# Test case: 2
# Location exists, Geocoding should not be invoked
it "test-2" do
  partner = Partner.create
  Location.create(country: 'India', state: 'Tamil Nadu', city: 'Chennai')
  expect(Geocoder).to_not receive(:coordinates)
  partner.update_locations('India', 'Tamil Nadu', 'Chennai')
end
    Here we mock the Geocoder to make sure it is not called.
Too much mocking
- Too much mocking is not good because its hard to follow the code. Its hard to differentiate what is tested and what is mocked.
 - If creating test-doubles starts taking too much effort, its better to create real objects
 
Thats all.
Thanks
Simplify testing with mocks and stubs
By Abhishek Yadav
Simplify testing with mocks and stubs
- 1,304