Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking Clojure protocols

Can one of the popular Java mocking frameworks like EasyMock or Mockito be used to mock Clojure protocols defined with defprotocol? If so, how?

like image 563
Ralph Avatar asked Apr 09 '11 13:04

Ralph


2 Answers

You should be able to mock protocols using any mock library. Under the covers, every protocol uses a Java interface as an implementation detail, and you could just mock that interface.

That said, don't do this! Mocking in Java is absurdly complex because of reflection, protection levels, final classes, etc. Any time you want a Clojure object that implements a protocol, simply call reify, e.g.

 (defprotocol Foo (method-a [_]) (method-b [_]))
 -> Foo

 (let [stub (reify Foo (method-a [_] :stubbed))] 
   (method-a stub))
 -> :stubbed

Note that you need not stub the methods you don't plan to call.

like image 84
Stuart Dabbs Halloway Avatar answered Nov 15 '22 16:11

Stuart Dabbs Halloway


It looks like the more recent versions of Midje provide this functionality quite nicely.

First, I would like to note that this kind of mocking is very commonly useful when splitting larger programs into components (e.g. that are assembled via dependency injection with libraries like Stuart Sierra's component library). If I've got some component that isolates a set of side-effect functions into a conceptual component I certainly want a testing framework that will let me inject a stand-in component so that I can:

  1. Write code that uses the component before it even exists (top-down).
  2. Test functions that use that component in isolation from the real implementation of the component.

You could use Mockito or some other library, but I agree such a solution is not going to be particularly elegant.

Unfortunately, protocols and records generate classes that Midje cannot hack into as easily as functions...so you do have to modify your code slightly:

(defrecord-openly SideEffectThing [connection]
 ISideEffect
 (persist [this thing] :unfinished)
 (read [this] :unfinished)
)

See Midje's documentation on production mode for details about how to make this modification not affect your production runtime.

By defining your component with defrecord-openly, you gain the ability to specify the behavior of the component's methods using Midje's "provided" mechanism:

(fact "you can test in terms of a record's methods"
  (let [obj (->SideEffectThing :fake-connection)]
   (user-of-side-effect-thing obj) => 0
   (provided
    (read obj) => -1)
  )
 )

You could, of course, avoid relying on a production type here (which I would advocate), and also avoid peppering defrecord-openly throughout your production code. In this case, just move SideEffectThing (as written above) into your test code. Then your component system in the application can plug in the real component, but your tests can be written against this unimplemented test version.

To be complete, I will compare the equivalent Java Mockito code with the above solution. In Java:

interface ISideEffect { int read(); void write(Object something); }
class SideEffectThing implements ISideEffect { ... }

// in test sources:
public class SomeOtherComponentSpec {
   private ISideEffect obj;
   @Before
   public void setup() { obj = mock(ISideEffect.class); }
   @Test
   public void does_something_useful() {
      when(obj.read()).thenReturn(-1);
      OtherComponent comp = new OtherComponent(obj);

      int actual = comp.doSomethingUseful();

      assertEquals(0, actual);
      verify(obj).read();
   }

this Java solution mocks out the component, specifies required behavior of that component, and then not only checks that the component itself works properly, but also that the component depended on the call to read() in some way. Mockito also supports pattern matching on arguments (and capture) to analyze how the component was used, if needed.

The Midje example above does much of that, and in a much clearer form. If you indicate (in the provided clause) that the function is called with specific arguments, then the test will fail if it isn't. If you specify that the function is called more than once (and it isn't), then that is a failure. For example, to indicate the read will be called 3 times and should return different values:

(fact "you can test in terms of a record's methods"
  (let [obj (->SideEffectThing :fake-connection)]
   (user-of-side-effect-thing obj) => 0
   (provided
    (read obj) => -1
    (read obj) => 6
    (read obj) => 99
    )
  )
 )

indicates that you expect read to be called three times, and that it should return the specified sequence of values. See the docs on prerequisites for more details, including how to specify exact number of times on a single function spec in provided, and how to indicate the function is never called.

like image 21
Tony K. Avatar answered Nov 15 '22 17:11

Tony K.