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
