Musings
muse: to turn something over in the mind meditatively and often inconclusively
Programmable mock objects

I keep rediscovering the idea of "programmable mock objects". There's nothing special about this technique, it's just quite effective and it's reasonably universal (although it's easier to implement in some languages than others). And frameworks exist for a range of languages. I've used Mocquer (Java) and Flex Mock (Ruby) looks promising.

In all honesty, before I even bothered looking for mock object libraries for Ruby I spent some time playing with a homegrown implementation. It grew out of a discussion with Marc and is really a refinement of a hacked together implementation that grew somewhat organically while testing a component that depends on a number of external components that made testing ... challenging.

The pattern for programmable mock objects is pretty straightforward. You create an instance of an object that can stand in for another object (i.e. a mock object). Depending on the language you're using and on the object you're mocking up this may be as simple as instantiating a subclass that overrides the public methods you're planning on testing. If you're using Java and you've defined a common interface then good for you and you can just instantiate a mock implementation of that interface. This can get challenging if you're using Java (mocking up final classes, or classes with final methods for example) but frameworks like Mocquer get around this by generating byte-code on the fly, producing an object the JVM can't differentiate from the class you're mocking up.

More dynamic languages like Ruby make mocking up objects trivial. In fact, Ruby's method_missing makes this as simple as:

class Mock
  def method_missing(method_id, *args)
    assert_method_expected(method_id.id2name, args)
  end
end

Any method (well almost, but close enough) that's called on an instance of Mock will be passed to the method_missing method at which point you can assert that you're expecting that method call.

But I've jumped the gun a little. What's that assert_expected? And how does it know what's expected? This is where programmable comes into it. The idea's pretty simple. You instantiate your mock object and then tell it what to expect (and what to return for each of those calls; without that your tests won't get very far). The exact approach varies from language to language but the general pattern is much the same. Here's an example of how you'd use a typical mock object in Ruby:

mock = Mock.new()
mock.expect(:sum 1, 2, 4).return(7)
mock.expect(:double, [1, 2, 3]).return([2, 4, 6])

Hopefully what's happening is reasonably clear: we're expecting a method called sum to be called with three parameters (and we want to return 7), followed by a call to double passing in an array (and returning an array with all the values doubled).

This is a pretty easy way to build test cases where you need to interact with external components that make testing difficult (like a network socket, or a running process). And you get to control exactly what each method returns, so testing weird edge cases is trivial.

In a bid to leave you with some actual working code, here's the Mock class I put together last night. I haven't used method_missing because I wanted to play with some of Ruby's meta-programming and this was an excuse to do so. The class is instantiated with a Class instance to mock and proceeds to define methods for all public methods on that class. In addition, it redefines all public class methods on the target class, so you can assert that class methods are called in the correct order too.

I've used whytheluckystiff's metaid.rb. This handy bit of code enhances Object to make metaprograming a little easier to follow. It looks like this:

class Object
  # The hidden singleton lurks behind everyone
  def metaclass; class << self; self; end; end
  def meta_eval &blk; metaclass.instance_eval &blk; end
  
  # Adds methods to a metaclass
  def meta_def name, &blk
    meta_eval { define_method name, &blk }
  end
  
  # Defines an instance method within a class
  def class_def name, &blk
    class_eval { define_method name, &blk }
  end
end

I also need a few exceptions defined:

class UnexpectedMethodError < Exception
  def initialize(method, expected=nil)
    if (expected)
      super("Method #{method} expected but not called (#{expected} expected instead)")
    else
      super("Method #{method} expected but not called")
    end
  end
end

class UnexpectedMethodTypeError < Exception
  def initialize(method, expected_class_method)
    if (expected_class_method)
      super("Class method #{method} expected but instance method called")
    else
      super("Instance method #{method} expected but class method called")
    end
  end
end

class UnexpectedArgumentsError < Exception
  def initialize(method, expected, received)
    super("Method #{method} expected arguments\n  #{expected.inspect}\n but received]\n  #{received.inspect}")
  end
end

class ExpectedMethodsError < Exception
  def initialize(remaining)
    super("Methods expected but not called:\n#{remaining}")
  end
end

And finally, the Mock class.

class Mock
  def initialize(target, options={})
    @target = target
    @expectations = []
    @seen_index = 0
    options = {:mock_class_methods=>true}.merge(options)
    target.public_instance_methods(false).each { |method| mock(method) }
    if (options[:mock_class_methods])
      target.singleton_methods.each { |method| rewrite(target, method) }
    end
  end

  def mock_attr(attr, mock_attr)
    instance_var_name = "@#{attr.to_s}"
    # Stash the attr's mock object in an instance variable
    instance_variable_set(instance_var_name.to_sym, mock_attr)
    # Define a getter that expects to be called and then returns the mock object
    self.class.class_def(attr) {assert_method_expected(attr, []); mock_attr}
    # Make sure we mock up the setter as for other methods
    mock("#{attr}=")
  end

  def expect(method, *args)
    @expectations << {:expect => [method, args]}
    self
  end

  def expect_class_method(method, *args)
    @expectations << {:expect => [method, args], :class_method=>true}
    self
  end

  def expect_attr(attr, method, *args)
    attr = [attr] if (attr.kind_of?(Symbol))
    attrstr = attr.collect{|a| a.to_s}.join(".")

    if (attr.empty?)
      expect(method, *args)
    else
      expect(attr[0])
      mock_attr = instance_variable_get("@#{attr[0].to_s}".to_sym)
      mock_attr.expect_attr(attr[1..-1], method, *args)
    end
  end

  def expect_attr_class_method(attr, method, *args)
    attr = [attr] if (attr.kind_of?(Symbol))
    attrstr = attr.collect{|a| a.to_s}.join(".")

    if (attr.empty?)
      expect_class_method(method, *args)
    else
      mock_attr = instance_variable_get("@#{attr[0].to_s}".to_sym)
      mock_attr.expect_attr_class_method(attr[1..-1], method, *args)
    end
  end

  def return(value=nil)
    @expectations[-1][:return] = value
  end

  def assert_method_expected(method, args)
    expected = @expectations[@seen_index]
    
    raise UnexpectedMethodError.new(method) if (expected.nil?)
    expected_method, expected_args = expected[:expect]
    raise UnexpectedMethodError.new(method, expected_method) if (method != expected_method)

    raise UnexpectedArgumentsError.new(method, expected_args, args) if (expected_args.length != args.length)
    args.each_with_index do
      |arg,idx|
      expected_arg = expected_args[idx]

      # Override == to handle comparisons differently
      raise UnexpectedArgumentsError.new(method, expected_args, args) unless (expected_arg == arg)
    end
    
    @seen_index += 1
    expected[:return]
  end
  private :assert_method_expected

  def assert_class_method_expected(method, args)
    retval = assert_method_expected(method, args)
    expected = @expectations[@seen_index-1]
    raise UnexpectedMethodTypeError.new(method, true) unless (expected[:class_method])
    retval
  end
  private :assert_class_method_expected

  def mock(method)
    self.class.class_def(method.to_sym) do
      |*args|
      assert_method_expected(method.to_sym, args) 
    end
  end

  def rewrite(target, method)
    instance = self
    target.meta_def(method.to_sym) do
      |*args| 
      instance.send(:assert_class_method_expected, method.to_sym, args) 
    end
  end

  def to_s
    list = []
    @expectations.each_with_index do 
      |e,i| 

      p = (i == @seen_index ? "* " : "  ")
      m = (e[:class_method] ? "#{@target.name}." : "") + e[:expect][0].to_s
      a = e[:expect][1].collect{|aa| aa.inspect}.join(", ")
      list << "#{p}#{m}(#{a}) -> #{e[:return]||'nil'}"
    end
    list.join("\n")
  end
end

This implementation's very barebones, and I've built in some special case handling for members exposed using attr because the particular case I'm using this for benefits from this.

Using this is pretty straightforward. Assuming the following test classes are defined:

class Test3
  def bar
    puts "Test3.bar()"
  end

  def Test3.class_bar
    puts "Test3.class_bar()"
  end
end

class Test2
  attr :test3

  def foo
    puts "Test2.foo()"
  end
end

class Test
  attr :test2

  def Test.a_class_method(a,b,c)
    puts "ORIG: a_class_method"
  end

  def an_instance_method(a,b,c)
    puts "ORIG: an_instance_method"
  end
end
we create appropriate mock objects (and set them up as mocked attributes) as follows:
mock = Mock.new(Test)
mock_test3 = Mock.new(Test3)
mock_test2 = Mock.new(Test2)
mock_test2.mock_attr(:test3, mock_test3)
mock.mock_attr(:test2, mock_test2)

We then "program" the mock object to expect a series of calls

mock.expect(:an_instance_method, 1, 2, 4).return("bar")
mock.expect_class_method(:a_class_method, 1).return("bar2")
mock.expect(:foo, 1).return("bar2")
mock.expect(:foo, 2, 3)
mock.expect_attr(:test2, :foo).return("bar")
mock.expect_attr([:test2, :test3], :bar).return("bar")
mock.expect_attr_class_method([:test2, :test3], :class_bar).return("bar")

Finally, we make the sequence of calls we've just scripted expectations for

mock.an_instance_method(1,2,4)
Test.a_class_method(1)
mock.foo(1)
mock.foo(2,3)
mock.test2.foo
mock.test2.test3.bar
Test3.class_bar

If you're following along at home try rearranging the method calls in the last block, or changing values or numbers of parameters. And try displaying the return value of some of those methods. You'll see they're just as you scripted.

Hopefully this has given someone out there some new ideas for testing. The world can always do with a few more testcases :-)

Posted at 08:48 PM

Published!

As I mentioned a while back the author of JavaFAQ.nu contacted me to ask for permission to publish my NIO tutorial.

He got back to me today to let me know he's finally gotten a chance to put it up. I just checked and it's still linked off the front page, but that probably won't last so the direct link is here.

I've actually had a fair bit of feedback over the past couple of weeks, both relating to Rox and to the NIO tutorial. Seems like a lot of people are using it (and getting some value out of it) which pleases me no end.

Posted at 08:16 PM

Life-lessons in a bowl of cornflakes

  • Don't rush. Rushing poured milk just increases your risk of splash-back.
  • Don't hesitate too long. Soggy cornflakes suck.
  • Anticipate the future. Don't over-sugar: it'll taste fine until all the sugar has dissolved and then it's too late.
  • Moderation. The third bowl always seems more appealing than it actually is.

Posted at 09:55 AM

Birthday Calendar

I'm terrible at dates, and in particular birthdays. I'm the worst phone-a-friend you could pick if your life suddenly depending on knowing someone's birthday.

Witness: we had coffee with Rory and Tarrin earlier and I missed all sorts of clues that his birthday had recently come and gone. Rory: happy birthday you sneaky bastard.

One of the benefits of marriage is that I now have access to a reliable calendar: my wife. Even better than Google's (prettier UI to say the least; an effective remote reminder service). Slowly, over the past year or so, I've been page faulting peoples' birthdays into Claire's diary for next year.

Apologies to all, but I promise next time round I'll be informed ahead of schedule.

Posted at 10:58 PM