Skip to content
Framework to aid in handrolling mock/spy objects.
Ruby
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.
gemfiles
lib
spec
.gitignore
.rvmrc
.travis.yml
Changelog.md
Rakefile
Readme.md
Readme.md.mountain_berry_fields
surrogate.gemspec
todo

Readme.md

Build Status

About

Handrolling mocks is the best, but involves more overhead than necessary, and usually has less helpful error messages. Surrogate addresses this by endowing your objects with common things that most mocks need. Currently it is only integrated with RSpec.

This codebase should be considered highly volatile until 1.0 release. The outer interface should be fairly stable, with each 0.a.b version having backwards compatibility for any changes to b (ie only refactorings and new features), and possible interface changes (though probably minimal) for changes to a. Depending on the internals of the code (anything not shown in the readme) is discouraged at this time. If you do want to do this (e.g. to make an interface for test/unit) let me know, and I'll inform you / fork your gem and help update it, for any breaking changes that I introduce.

New Syntax

Recently (v0.5.1), a new syntax was added:

OldNew
.should have_been_told_to.was told_to
.should have_been_asked_for_its.was asked_for
.should have_been_asked_if.was asked_if
.should have_been_initialized_with.was initialized_with

If you want to switch over, here is a shell script that should get you pretty far:

find spec -type file |
  xargs ruby -p -i.old_syntax \
  -e 'gsub /should(_not)?(\s+)have_been_told_to/,               "was\\1\\2told_to"' \
  -e 'gsub /should(_not)?(\s+)have_been_asked_(if|for)(_its)?/, "was\\1\\2asked_\\3"' \
  -e 'gsub /should(_not)(\s+)have_been_initialized_with/,       "was\\1\\2initialized_with"' \

Features

  • Declarative syntax
  • Support default values
  • Easily override values
  • RSpec matchers for asserting what happend (what was invoked, with what args, how many times)
  • RSpec matchers for asserting the Mock's interface matches the real object
  • Support for exceptions
  • Queue return values
  • Initialization information is always recorded

Usage

Endow a class with surrogate abilities

class Mock
  Surrogate.endow self
end

Define a class method by using define in the block when endowing your class.

class MockClient
  Surrogate.endow self do
    define(:default_url) { 'http://example.com' }
  end
end

MockClient.default_url # => "http://example.com"

Define an instance method by using define outside the block after endowing your class.

class MockClient
  Surrogate.endow self
  define(:request) { ['result1', 'result2'] }
end

MockClient.new.request # => ["result1", "result2"]

If you care about the arguments, your block can receive them.

class MockClient
  Surrogate.endow self
  define(:request) { |limit| limit.times.map { |i| "result#{i.next}" } }
end

MockClient.new.request 3 # => ["result1", "result2", "result3"]

You don't need a default if you set the ivar of the same name (replace ? with _p for predicates, since you can't have question marks in ivar names)

class MockClient
  Surrogate.endow self
  define(:initialize) { |id| @id, @connected_p = id, true }
  define :id
  define :connected?
end
MockClient.new(12).id # => 12

Override defaults with will_<verb> and will_have_<noun>

class MockMP3
  Surrogate.endow self
  define :play # defaults are optional, will raise error if invoked without being told what to do
  define :info
end

mp3 = MockMP3.new

# verbs
mp3.will_play true
mp3.play # => true

# nouns
mp3.will_have_info artist: 'Symphony of Science', title: 'Children of Africa'
mp3.info # => {:artist=>"Symphony of Science", :title=>"Children of Africa"}

Errors get raised

class MockClient
  Surrogate.endow self
  define :request
end

client = MockClient.new
client.will_have_request StandardError.new('Remote service unavailable')

begin
  client.request
rescue StandardError => e
  e # => #<StandardError: Remote service unavailable>
end

Queue up return values

class MockPlayer
  Surrogate.endow self
  define(:move) { 20 }
end

player = MockPlayer.new
player.will_move 1, 9, 3
player.move # => 1
player.move # => 9
player.move # => 3

You can define initialize

class MockUser
  Surrogate.endow self do
    define(:find) { |id| new id }
  end
  define(:initialize) { |id| @id = id }
  define(:id) { @id }
end

user = MockUser.find 12
user.id # => 12

RSpec Integration

Currently only integrated with RSpec, since that's what I use. It has some builtin matchers for querying what happened.

Load the RSpec matchers.

require 'surrogate/rspec'

Last Instance

Access the last instance of a class

class MockMp3
  Surrogate.endow self
end

mp3_class = MockMp3.clone # because you don't want to mutate the singleton
mp3 = mp3_class.new
mp3_class.last_instance.equal? mp3 # => true

Nouns

Given this mock and assuming the following examples happen within a spec

class MockMP3
  Surrogate.endow self
  define(:info) { |song='Birds Will Sing Forever'| 'some info' }
end

Check if was invoked with have_been_asked_for_its

mp3.should_not have_been_asked_for_its :info
mp3.info
mp3.should have_been_asked_for_its :info

Invocation cardinality by chaining times(n)

mp3.info
mp3.info
mp3.should have_been_asked_for_its(:info).times(2)

Invocation arguments by chaining with(args)

mp3.info :title
mp3.should have_been_asked_for_its(:info).with(:title)

Supports RSpec's matchers (no_args, hash_including, etc)

mp3.info
mp3.should have_been_asked_for_its(:info).with(no_args)

Cardinality of a specific set of args with(args) and times(n)

mp3.info :title
mp3.info :title
mp3.info :artist
mp3.should have_been_asked_for_its(:info).with(:title).times(2)
mp3.should have_been_asked_for_its(:info).with(:artist).times(1)

Verbs

Given this mock and assuming the following examples happen within a spec

class MockMP3
  Surrogate.endow self
  define(:play) { true }
end

Check if was invoked with have_been_told_to

mp3.should_not have_been_told_to :play
mp3.play
mp3.should have_been_told_to :play

Also supports the same with(args) and times(n) that nouns have.

Initialization

Query with have_been_initialized_with, which is exactly the same as saying have_been_told_to(:initialize).with(...)

class MockUser
  Surrogate.endow self
  define(:initialize) { |id| @id = id }
  define :id
end
user = MockUser.new 12
user.id.should == 12
user.should have_been_initialized_with 12

Predicates

Query qith have_been_asked_if, all the same chainable methods from above apply.

class MockUser
  Surrogate.endow self
  define(:admin?) { false }
end

user = MockUser.new
user.should_not be_admin
user.will_have_admin? true
user.should be_admin
user.should have_been_asked_if(:admin?).times(2)

class MockUser

Substitutability

After you've implemented the real version of your mock (assuming a top-down style of development), how do you prevent your real object from getting out of synch with your mock?

Assert that your mock has the same interface as your real class. This will fail if the mock inherits methods which are not on the real class. It will also fail if the real class has any methods which have not been defined on the mock or inherited by the mock.

Presently, it will ignore methods defined directly in the mock (as it adds quite a few of its own methods, and generally considers them to be helpers). In a future version, you will be able to tell it to treat other methods as part of the API (will fail if they don't match, and maybe record their values).

class User
  def initialize(id)end
  def id()end
end

class MockUser
  Surrogate.endow self
  define(:initialize) { |id| @id = id }
  define :id
end

# they are the same
MockUser.should substitute_for User

# mock has extra method
MockUser.define :name
MockUser.should_not substitute_for User

# the same again via inheritance
class UserWithName < User
  def name()end
end
MockUser.should substitute_for UserWithName

# real class has extra methods
class UserWithNameAndAddress < UserWithName
  def address()end
end
MockUser.should_not substitute_for UserWithNameAndAddress

Sometimes you don't want to have to implement the entire interface. In these cases, you can assert that the methods on the mock are a subset of the methods on the real class.

class User
  def initialize(id)end
  def id()end
  def name()end
end

class MockUser
  Surrogate.endow self
  define(:initialize) { |id| @id = id }
  define :id
end

# doesn't matter that real user has a name as long as it has initialize and id
MockUser.should substitute_for User, subset: true

# but now it fails b/c it has no address
MockUser.define :address
MockUser.should_not substitute_for User, subset: true

Blocks

When your method is invoked with a block, you can make assertions about the block.

Note: Right now, block error messages have not been addressed (which means they are probably confusing as shit)

Before/after hooks (make assertions here)

class MockService
  Surrogate.endow self
  define(:create) {}
end

describe 'something that creates a user through the service' do
  let(:old_id) { 12 }
  let(:new_id) { 123 }

  it 'updates the user_id and returns the old_id' do
    user_id = old_id
    service = MockService.new

    service.create do |user|
      to_return = user_id
      user_id = user[:id]
      to_return
    end

    service.should have_been_told_to(:create).with { |block|
      block.call_with({id: new_id})              # this will be given to the block
      block.returns old_id                       # provide a return value, or a block that receives the return value (where you can make assertions)
      block.before { user_id.should == old_id }  # assertions about state of the world before the block is called
      block.after  { user_id.should == new_id }  # assertions about the state of the world after the block is called
    }
  end
end

How do I introduce my mocks?

This is known as dependency injection. There are many ways you can do this, you can pass the object into the initializer, you can pass a factory to your class, you can give the class that depends on the mock a setter and then override it whenever you feel it is necessary, you can use RSpec's #stub method to put it into place.

Personally, I use Deject, another gem I wrote. For more on why I feel it is a better solution than the above methods, see it's readme.

But why write this?

Need to put an explanation here soon. In the meantime, I wrote a blog that touches on the reasons.

Special Thanks

  • Kyle Hargraves for changing the name of his internal gem so that I could take Surrogate
  • David Chelimsky for pairing with me to make Surrogate integrate better with RSpec
  • Corey Haines for pairing on substitutability with me
  • Enova for giving me time and motivation to work on this during Enova Labs.
  • 8th Light for giving me time to work on this during our weekly Wazas, and the general encouragement and interest
Something went wrong with that request. Please try again.