Kerry Buckley

What’s the simplest thing that could possibly go wrong?

An issue with mock-driven development in dynamically-typed languages

leave a comment

First, let me make it clear that I really like BDD, I really like mocks, and I really like dynamic languages. RSpec does a pretty good job of combining all three.

However, there's one disadvantage that duck-typed languages suffer when it comes to using mocks to drive the design of the interfaces between objects.

The following example is lifted from the excellent paper Mock Roles, not Objects, by Steve Freeman, Nat Pryce, Tim Mackinnon and Joe Walnes. If you haven't already read it, it's well worth it.

Here's a test case:

JAVA:
  1. public class TimedCacheTest {
  2.   static final Object KEY = 1;
  3.   static final Object KEY2 = 1;
  4.   static final Object VALUE = "one";
  5.   static final Object VALUE2 = "two";
  6.  
  7.   public void testLoadsObjectThatIsNotCached() {
  8.     ObjectLoader mockLoader = mock(ObjectLoader.class);
  9.     TimedCache cache = new TimedCache(mockLoader);
  10.  
  11.     mockLoader.expect(once()).method("load").with( eq(KEY) )
  12.         .will(returnValue(VALUE));
  13.     mockLoader.expect(once()).method("load").with( eq(KEY2) )
  14.         .will(returnValue(VALUE2));
  15.  
  16.     assertSame( "should be first object", VALUE, cache.lookup(KEY) );
  17.     assertSame( "should be second object", VALUE2, cache.lookup(KEY2) );
  18.  
  19.     mockLoader.verify();
  20.   }
  21. }

And here's some code to pass the test:

JAVA:
  1. public class TimedCache {
  2.   private ObjectLoader loader;
  3.  
  4.   public TimedCache(ObjectLoader loader) {
  5.     this.loader = loader;
  6.   }
  7.  
  8.   public Object lookup(Object key) {
  9.     return loader.load(key);
  10.   }
  11. }

In order to get this to compile1, you also need to introduce an interface:

JAVA:
  1. public interface ObjectLoader {
  2.   Object load(Object theKey);
  3. }

When you've finished specifying the behaviour of TimedCache, you simply move on to an implementation of ObjectLoader, and repeat the process.

Now here's something similar in Ruby. First the test:

RUBY:
  1. KEY = 1
  2. KEY2 = 2
  3. VALUE = "one"
  4. VALUE2 = "two"
  5.  
  6. describe "An object that is not cached" do
  7.   before :each do
  8.     @mock_loader = mock "loader"
  9.     @cache = TimedCache.new @mock_loader
  10.   end
  11.  
  12.   it "should be loaded" do
  13.     @mock_loader.should_receive(:load).with(KEY).and_return VALUE
  14.     @mock_loader.should_receive(:load).with(KEY2).and_return VALUE2
  15.     @cache.lookup(KEY).should == VALUE
  16.     @cache.lookup(KEY2).should == VALUE2
  17.   end
  18. end

And the code to make it pass:

RUBY:
  1. class TimedCache
  2.   def initialize loader
  3.     @loader = loader
  4.   end
  5.  
  6.   def lookup key
  7.     @loader.load key
  8.   end
  9. end

The difference here is that Ruby doesn't have interfaces. The mock object loader is just an object of no particular class, with a bunch of methods on it (OK, just the one in this case), which you then have to remember to include in the real implementation.

I wonder whether it would be useful to be able to print out a list of the methods that you've mocked, to give you a starting point when implementing the classes you've mocked. Here's a very quick-and-dirty hack to Spec::Mocks::Proxy that prints a list to the console while the test's running:

RUBY:
  1. module Spec
  2.   module Mocks
  3.     class Proxy
  4.         ...
  5.       def verify_expectations
  6.         # HACK STARTS HERE!
  7.         puts "  mocked on #{@name}:"
  8.         @expectations.each do |expectation|
  9.           puts "    #{expectation.sym.to_s}(#{expectation.expected_args.join ', '})"
  10.         end
  11.         #HACK ENDS HERE
  12.         @expectations.each do |expectation|
  13.           expectation.verify_messages_received
  14.         end
  15.       end
  16.       ...

Here's the output:

An object that is not cached
  mocked on loader:
    load(1)
    load(2)
- should be loaded

Really this probably belongs in a custom formatter, but it doesn't look like the right hooks currently exist to implement it that way.

Technorati Tags: , , ,


1You'll also have to fix any typos I've introduced – I haven't actually run any of this code.

Written by Kerry

May 29th, 2007 at 10:09 am

Posted in Agile,Java,Ruby

Leave a Reply