Mock Object Injection

Posted by James Mead Thu, 29 Nov 2007 12:17:48 GMT

A few months back, in my Introduction to Mock Objects talk at LRUG, I talked about “Mock Object Injection”. At the time I described a number of different ways of replacing a production object with a Mock Object using Mocha. I remember that at the meeting, James Adam (who has since joined the team at Reevoo) asked me why I didn’t like the Any Instance Stub Injection technique.

I’m not sure I gave him a very convincing response and I’ve been meaning for ages to have a better go at explaining what I think are the pros and cons of each of the techniques I mentioned. Here’s the list of techniques with the ones I like best at the top. I still haven’t done a very good job, but I’d be interested to hear what other people think so that I can try and improve my understanding.

Constructor Injection

The ClassUnderTest allows its dependencies to be passed in as parameters to its constructor. A mock object is passed in as a replacement for the “real” collaborator. It may be convenient to specify the production collaborator as a default parameter value.

Advantages

The dependencies of the ClassUnderTest are explicit.

Disadvantages

Can’t think of any at the moment.

  class ClassUnderTest
    def initialize(dependency = Collaborator.new)
      @dependency = dependency
    end
    def do_something
      # use @dependency
    end
  end

  collaborator = mock('collaborator')
  # constructor parameter injection
  instance_under_test = ClassUnderTest.new(collaborator)
  instance_under_test.do_something

Parameter Injection

The ClassUnderTest allows its dependencies to be passed in as parameters to the method under test. A mock object is passed in as a replacement for the “real” collaborator. It may be convenient to specify the production collaborator as a default parameter value.

Advantages

The dependencies of the method under test are explicit.

Disadvantages

Can’t think of any at the moment.

  class ClassUnderTest
    def do_something(local_dependency = Collaborator.new)
      # use local_dependency
    end
  end

  collaborator = mock('collaborator')
  instance_under_test = ClassUnderTest.new
  # method parameter injection
  instance_under_test.do_something(collaborator)

Stubbed New Method Injection

Use Mocha’s Object#stubs to temporarily replace Collaborator#new with a stub implementation that returns a mock object.

Advantages

Better than Any Instance Stub Injection, because you can have more control over different instances of Collaborator.

Disadvantages

Dependencies of the ClassUnderTest are hidden and not explicit.

  class ClassUnderTest
    def initialize
      @dependency = Collaborator.new
    end
    def do_something
      # use @dependency
    end
  end

  collaborator = mock('collaborator')
  # stubbed new method injection
  Collaborator.stubs(:new).returns(collaborator)
  instance_under_test = ClassUnderTest.new
  instance_under_test.do_something

Writer Method Injection

Use an attribute writer method to replace the “real” collaborator with a mock object.

Disadvantages

The ClassUnderTest has to unnecessarily expose a way to modify its internal state. The test is coupled to the implementation of the ClassUnderTest.

  class ClassUnderTest
    attr_writer :dependency
    def initialize
      @dependency = Collaborator.new
    end
    def do_something
      # use @dependency
    end
  end

  collaborator = mock('collaborator')
  instance_under_test = ClassUnderTest.new
  # writer method injection
  instance_under_test.dependency = collaborator
  instance_under_test.do_something

Stubbed Private Method Injection

Use partial mocking to temporarily replace a private builder method with a stubbed version of the method.

Disadvantages

The test is coupled to the implementation of the ClassUnderTest. The partial mocking of the instance_under_test means that the test is not testing a pristine instance of the ClassUnderTest, but a modified one. It also means that the boundaries between test code and production code are less clear.

  class ClassUnderTest
    def do_something
      local_dependency = build_collaborator()
      # use local_dependency
    end
    private
    def build_collaborator
      Collaborator.new
    end
  end

  collaborator = mock('collaborator')
  instance_under_test = ClassUnderTest.new
  # stubbed private method injection
  instance_under_test.stubs(:build_collaborator).returns(collaborator)
  instance_under_test.do_something

Any Instance Stub Injection

Use Mocha’s Class#any_instance method to temporarily replace the method on a collaborator with a stub method.

Disadvantages

The stubbed method is applied to all instances of the collaborating class. If the instance_under_test interacts with the stubbed method on more than one instance of the collaborating class, it isn’t possible to specify different behaviour for the stubbed method on each instance. Even if the instance_under_test only interacts with the stubbed method on one instance of the collaborating class, the test is specifying more stubbed behaviour than strictly necessary which could lead to false positives.

  class ClassUnderTest
    def do_something
      local_dependency = Collaborator.new
      return local_dependency.do_stuff
    end
  end

  # any_instance stub injection
  Collaborator.any_instance.stubs(:do_stuff).return('something useful')
  instance_under_test = ClassUnderTest.new
  instance_under_test.do_something

Instance Variable Set Injection

Use Object#instance_variable_set to replace the reference to a collaborator with a mock object.

Disadvantages

The test is coupled to the implementation of the ClassUnderTest. In particular the test is coupled to the supposedly private instance variable. In my opinion, it would be more honest to expose the instance variable by adding an attribute writer and using Writer Method Injection.

  class ClassUnderTest
    def initialize
      @dependency = Collaborator.new
    end
    def do_something
      # use @dependency
    end
  end

  collaborator = mock('collaborator')
  instance_under_test = ClassUnderTest.new
  # instance_variable_set injection
  instance_under_test.instance_variable_set(:@dependency, collaborator)
  instance_under_test.do_something

Tags , , , , , ,  | 1 comment

Mock Objects in Ruby

Posted by James Mead Sun, 22 Jul 2007 20:48:00 GMT

As promised I’ve finally got round to making the S5 slides of my LRUG talk available here.

You can use various keys to navigate the presentation. If you just want to quickly scan through the content, you might find the outline view useful (press the “T” key to toggle between slideshow & outline modes).

Tags , , , , , ,  | no comments

Fixtures, mock objects or in-memory ActiveRecord objects?

Posted by James Mead Sat, 23 Dec 2006 20:00:00 GMT

Wilson Bilkovich has posted an article about mocking ActiveRecord objects.

A new method mock_model is defined that builds a mock object which will respond the same way as a real ActiveRecord object. As I understand it, this means he can replace…

  @campaign = mock("campaign")
  @campaign.stub!(:is_a?).and_return(true)
  @campaign.stub!(:new_record?).and_return(false)
  @campaign.stub!(:id).and_return(rand(1000))

with…

  mock_model :campaign

Although I agree with him that using fixtures is not a good idea, why not use a real ActiveRecord object…

  @campaign = Campaign.new

Sometimes due to the way ActiveRecord couples your models to the database, it becomes essential to have a model in the database and not just in memory. In which case why not just do this…

  @campaign = Campaign.create!

I’ve written up a couple more thoughts here and here.

Tags , , , , , ,  | 1 comment

About Mock Objects

Posted by James Mead Mon, 02 Oct 2006 13:19:00 GMT

It’s good to see that Steve & Nat have resurrected mockobjects.com and are starting to publish new articles. They describe the site as…

About Mock Objects, a technique for improving the design of code within Test-Driven Development.

There are also some new developments in their JMock library.

Tags , , , , , , ,  | no comments