Apr 29 2008

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