Ruby mock instance
The common pattern for mocking class instances in Ruby involves setting an expectation on the
new
method of the class to return an object of the specified type. Consider the following
User
and
Account
classes.
class User attr_reader :name, :email def initialize(name, email) @name, @email = name, email end def contact send_email(@email) end end class Account def self.create(name, email) user = User.new(name, email) user.contact end end
The test for the
Account#create
method would look like this:
require "rubygems" require "mocha" require "test/unit" class AccountTest < Test::Unit::TestCase def test_emails_user_on_account_creation user = User.new("", "") user.expects(:contact) User.expects(:new).with("Kirk", "kirk@metallica.com").returns(user) Account.create("Kirk", "kirk@metallica.com") end end
As an alternative, and this is the version I see most often used, a duck typed mock can be used in the place of the "real"
User
object.
class AccountTest < Test::Unit::TestCase def test_emails_user_on_account_creation user = mock user.expects(:contact) User.expects(:new).with("Kirk", "kirk@metallica.com").returns(user) Account.create("Kirk", "kirk@metallica.com") end end
Both implementations have some potentially undesirable issues worth taking into consideration. None of the two is
DRY,
since in both cases we need to set a bespoke expectation for the object's instantiation. In the first example, we are actually instantiating a "real"
User
.
This compromises the dependency neutrality of the test, coupling it with
User
's
initialization implementation. The second example, despite its relative convenience, doesn't fully respect the contract between
Account
and
User
.
The interaction specification is inaccurate, because, as far as the application code under test is concerned,
User#new
doesn't return an object of type
Mocha::Mock
,
it returns an instance of
User
.
The following code can be useful in an effort to DRY up code as the one under discussion, whilst driving such interaction specifications closer to the actual contracts they are meant to express.
class Object def self.mock_instance(*args) class_eval do alias original_initialize initialize def initialize()end end instance = new expects(:new).with(*args).returns(instance) class_eval do alias initialize original_initialize undef original_initialize end return instance end end
Here is how
AccountTest
can be written with the above
Mocha
extension in place.
class AccountTest < Test::Unit::TestCase def test_emails_user_on_account_creation User.mock_instance("Kirk", "kirk@metallica.com").expects(:contact) Account.create("Kirk", "kirk@metallica.com") end end
If
RSpec
is your flavor, the same can be achieved with minor modifications in the
Spec::Mocks::Methods
module.
module Spec::Mocks::Methods def mock_instance(*args) class_eval do alias original_initialize initialize def initialize()end end instance = new should_receive(:new).with(*args).and_return(instance) class_eval do alias initialize original_initialize undef original_initialize end return instance end end
With the above we can use
mock_instance
in specs as such.
describe Account do it "should email user on account creation" do User.mock_instance("Kirk", "kirk@metallica.com").should_receive(:contact) Account.create("Kirk", "kirk@metallica.com") end end