Simple Ruby Mocking
I mentioned at the end of my last post on testing that I wrote some code to do mocking for my unit tests in Ruby. Writing a small mock library was very much reinventing the wheel, but I needed to do it to earn a deeper understanding of mocks.
I’m writing some code (“Fetcher”) to talk to a POP3 server, fetch mail, and pass it off to another process. One of the tests deals with what happens when the POP3 server is down or otherwise unreachable.
` def test_setup_server_down pop3 = Mock.new pop3.expect(:new, [MAIL_SERVER, MAIL_POP3_PORT]){ raise Timeout::Error.new(“execution expired”) } f = Fetcher.new(pop3) # further assertions that f acts correctly end `{lang=”ruby”}
This says that the Mock object expects a call to the new method with the given arguments, and when the call happens it runs the block. The block could return anything, but in this case it raises the same error as Net::Pop3 does when it can’t contact the server. After that the test can go on to make whatever assertions it needs to verify that the exception was handled properly.
The Mock object has a list of calls
it expects to see and keeps a list of how it’s been called
(yes, this could just be one list with an index but I thought it was mentally simpler this way). The test sets up what calls it should expect
to see with what arguments (or blank for any) and block to run (or blank for none). When a method is called on the mock object, method_missing
logs the call and executes the given block (raising a fuss if the call didn’t match what it expected).
class Mock attr_reader :calls, :called
# the stub arg makes it just record all calls def initialize @calls = [] @called = [] end
# Pass nil for args to ignore the actual args in the call. # Proc is optional; default is empty proc returning nil. def expect(method, *args, &proc) @calls << {:method => method, :args => args.first, :proc => (proc or Proc.new{})} end
def method_missing(method, *args) @called << {:method => method, :args => args}
expect = @calls.shift raise “Unexpected mock call #{method.to_s}(#{args.join(‘, ‘)})” if expect.nil? raise “Wrong mock call #{method.to_s}(#{args.join(‘, ‘)}); expected #{expect[:method]}(#{expect[:args].join(‘, ‘)})” if method != expect[:method] or (expect[:args] != nil and args != expect[:args]) expect[:proc].call(*args) end end
It’s a straightforward little object, and I also added some code to raise a fuss if expected calls weren’t made. This does have the downside that any tests defining their own teardown
need to call super
.
class Mock def fail_if_not_empty # Empty the call stack so that this obj doesn’t throw errors for # every later test between now and this object getting gc’d calls, @calls = @calls, [] raise “Mock calls uncalled: n” + calls.collect { |call| “#{call[:method]}(#{call[:args]} { #{call[:proc] })” }.join(“ “) unless calls.empty? end end
class Test::Unit::TestCase def teardown finish_mocks end
def finish_mocks ObjectSpace.each_object(Mock) do |m| m.fail_if_not_empty end end end
This has been a handy piece of code to test the code I’ve written in the last two weeks, but it’s not good enough. I have to use a technique called dependency injection to test Fetcher.new
, where the outside code passes it a POP3 object instead of its initialize
just using Net::POP3. Useful for testing, but my code is badly repetitive when all the instantiation calls have to do this exact same setup. (As an aside, Jacob Proffitt recently started an interesting conversation on dependency injection, took criticism, and responded. Good reading.)
I was pondering how to extend the Mock object to let me mock class methods (eg a call to Net::POP3.new
) when I realized I’d gone as far as I should down the do-it-yourself road. I’d heard of Mocha and it took me all of ten minutes to think to look at its cheat sheet where that’s the first example.
After spending two hours or so writing and tweaking this code, the best thing for it is to be thrown away. I’ve learned about mocking by doing it and I’m better-prepared to understand someone else’s larger and better library.