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 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.
Features
- Mock does not diverge from real class because compares signatures of mocks to signatures of the real class.
- 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
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"]
Ruby's attr_accessor
, attr_reader
, attr_writer
are mimicked with define_accessor
, define_reader
, define_writer
.
You can pass a block to define_accessor
and define_reader
if you would like to give it a default_value.
class MockClient
Surrogate.endow self
define_accessor :default_url
end
client = MockClient.new
client.default_url # => nil
client.default_url = 'http://whatever.com'
client.default_url # => "http://whatever.com"
If you expect arguments, your block should receive them. This prevents the issues with dynamic mocks where arguments and parameters can diverge. It may seem like more work when you have to write the arguments explicitly, but you only need to do this once, and then you can be sure they match on each invocation.
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, and !
with _b
for bang methods, since you can't have question marks or bangs in ivar names).
class MockClient
Surrogate.endow self
define(:initialize) { |id| @id, @connected_p, @reconnect_b = id, true, true }
define :id
define :connected?
define :reconnect!
end
MockClient.new(12).id # => 12
Override defaults with will_<verb>
and will_have_<noun>
class MockMP3
Surrogate.endow self
define :play # default behavior is to do nothing, same as empty block yielding nothing
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
Use clone to avoid altering state on the class. You can pass it key/value pairs to mass initialize the clone (I think it's probably best to avoid this kind of state on classes, but do support it)
class MockUser
Surrogate.endow self do
define(:find) { new }
end
end
# using a clone
user_class = MockUser.clone
user_class.find
user_class.was told_to :find
MockUser.was_not told_to :find
# initializing the clone
expect(MockUser.clone(find: nil).find).to be_nil
Mass initialize with .factory(key: value)
, this can be turned off by passing
factory: false
as an option to Surrogate.endow, or given a different name if
the value is the new name.
# default factory method
class MockUserWithFactory
Surrogate.endow self
define(:name) { 'Samantha' }
define(:age) { 83 }
end
user = MockUserWithFactory.factory name: 'Jim', age: 26
user.name # => "Jim"
user.age # => 26
# use a different name
class MockUserWithRenamedFactory
Surrogate.endow self, factory: :construct
define(:name) { 'Samantha' }
define(:age) { 83 }
end
user = MockUserWithRenamedFactory.construct name: 'Milla'
user.name # => "Milla"
user.age # => 83
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 was asked_for
(was
is an alias of should
, and asked_for
is a custom matcher)
mp3.was_not asked_for :info
mp3.info
mp3.was asked_for :info
Invocation cardinality by chaining times(n)
mp3.info
mp3.info
mp3.was asked_for(:info).times(2)
Invocation arguments by chaining with(args)
mp3.info :title
mp3.was asked_for(:info).with(:title)
Supports RSpec's matchers (no_args
, hash_including
, etc)
mp3.info
mp3.was asked_for(: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.was asked_for(:info).with(:title).times(2)
mp3.was asked_for(: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 was told_to
mp3.was_not told_to :play
mp3.play
mp3.was told_to :play
Also supports the same with(args)
and times(n)
that nouns have.
Initialization
Query with was initialized_with
, which is exactly the same as saying was told_to(:initialize).with(...)
class MockUser
Surrogate.endow self
define(:initialize) { |id| @id = id }
define :id
end
user = MockUser.new 12
expect(user.id).to eq 12
user.was initialized_with 12
Predicates
Query qith was asked_if
, all the same chainable methods from above apply.
class MockUser
Surrogate.endow self
define(:admin?) { false }
end
user = MockUser.new
expect(user).to_not be_admin
user.will_have_admin? true
expect(user).to be_admin
user.was asked_if(:admin?).times(2)
Substitutability
Facets of substitutability: method existence, argument types, (and soon argument names)
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 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
expect(User).to substitute_for MockUser
# they differ
MockUser.define :name
expect(User).to_not substitute_for MockUser
# signatures don't match (you can turn this off by passing `types: false` to substitute_for)
class UserWithWrongSignature
def initialize()end # no id
def id()end
end
expect(UserWithWrongSignature).to_not substitute_for MockUser
# parameter names don't match
class UserWithWrongParamNames
def initialize(name)end # real one takes an id
def id()end
end
expect(UserWithWrongParamNames).to_not substitute_for MockUser, names: true
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
expect(User).to substitute_for MockUser, subset: true
# but now it fails b/c it has no address
MockUser.define :address
expect(User).to_not substitute_for MockUser, 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.was 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 { expect(user_id).to eq old_id } # assertions about state of the world before the block is called
block.after { expect(user_id).to eq 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
- 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
- The people who have paired with me on this: Corey Haines on substitutability, Kori Roys on
#last_instance
, Wai Lee on the new assertion syntax - The people who have contributed: [Michael Baker](with m://github.com/michaelbaker) on the readme.