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