marick / Midje

Midje provides a migration path from clojure.test to a more flexible, readable, abstract, and gracious style of testing
MIT License
1.69k stars 129 forks source link

mocking can not be used with protocols #17

Closed bmabey closed 13 years ago

bmabey commented 13 years ago

Since function/method dispatch happens at the JVM layer for protocols I suspect the regular binding trick won't work. Here is an example of it not working:

(defprotocol Foo
  (bar [foo x])
  (baz [foo z]))

(defrecord Bar [y]
  Foo
  ;;(bar [foo x] (* (:y foo) x))
  (bar [foo x] ; signature needs to be present to avoid an AbstractMethodError
       0)
  (baz [foo z] (+ (bar foo z) z)))

(fact
 (let [foo5 (Bar. 5)]
   (baz foo5 2) => 12
   (provided
    (bar foo5 2) => 10))

Output:

FAIL at (....)
Expected: 12
  Actual: 2

I haven't played with this idea, but for midje (or any other clojure mocking tool for that matter) to support protocols wrapping a Java mocking library may be a viable option. WDYT?

marick commented 13 years ago

What I think: grumble

It's a definite issue. Still, sometimes I think Clojure isn't so much a leaky abstraction over the JVM as a powerful sump pump sucking Java badness into the world of Lisp.

marick commented 13 years ago

Here's another example that I fooled with while trying to find a workaround:


(defprotocol FooProtocol
  (strcat [this from-function]))

(defrecord FooRecord [from-record]
  FooProtocol
  (strcat [this from-function]
          (str "record: " from-record " function: " from-function)))

(defn foo-record-user [record]
  (strcat record "foo-record-user"))

(fact
  (let [foo (FooRecord. "rec")]
    (foo-record-user foo) => "record: rec function: foo-record-user"))

(fact
  (let [foo (FooRecord. "rec")]
    (foo-record-user foo) => "33"
    (provided (strcat foo) => "33")))
stuarthalloway commented 13 years ago

Is something wrong with:

(defprotocol Foo (bar [_]))
(with-redefs [bar (fn [_] :no-problem)] (bar nil))
bmabey commented 13 years ago

I had not seen with-redefs.... is this only in 1.3?

I just pulled down 1.3.0-beta1 and tried it out. It appears to not work on protocol functions either:

user> (defprotocol Foo (bar [_]))
Foo
user> (defrecord FooRecord [x] Foo (bar [_] (inc x)))
user.FooRecord
user> (with-redefs [bar (fn [x] :blah)] (bar (FooRecord. 5)))
6

Am I doing something wrong?

It seems to work just fine with "regular" functions though:

user> (defn foo [x] (inc x))
#'user/foo
user> (with-redefs [foo dec] (foo 4))
3
stuarthalloway commented 13 years ago

It's just a matter of choosing the right thing to mock. Instead of mocking the function bar, you will need to mock the object FooRecord, e.g. using reify. The good news is that the approach should seem familiar to OO folks, who are always mocking the objects, not the fns. :-)

Since systems built around mock-based testing tend to expose plenty of injection points, I would expect this to be straightforward. If not, I would be happy to look at a larger example and propose ideas.

It is true that mocking plain fns and mocking objects will be two different things. That seems ok to me. Mocking is inherently a white-box exercise requiring that you deal with the specifics of your platform.

bmabey commented 13 years ago

Yeah, I have been taking a similar approach in my tests. In certain cases I will define dummy objects (records) whose implementation I control so I can verify that the interactions are correct. However, this seems like a heavy-handed solution if all you need to do is stub a single function/method call for that record. For the use cases listed above this approach makes sense.

I'd like to beable to say something like (stub FooRecord foo #( ... )) or . (stub foo-record-instance foo #( ... )) I don't think this is possible because you will get IllegalArgumentException class user.FooRecord already directly implements interface user.Foo for protocol:#'user/Foo. I think the only way to get behaviour like this would be to wrap the original foo-record-instance in a temporary stub/mock proxy.

marick commented 13 years ago

Side issue: You wrote, "Mocking is inherently a white-box exercise requiring that you deal with the specifics of your platform." That's not the style I'm trying to promote with Midje. I'm trying to make tests into statements about the logical relationship between functions. It's all about removing even more inessential [mental] complexity from the design than Clojure allows.

However, protocols are (or seem to me) a leaky abstraction, and I expect you're right that mocking objects is the right way to go about it. I might be able to unify the notation with some ideas about testing inner functions.

stuarthalloway commented 13 years ago

I'd prefer a non-unified notation that let me get at stuff, at least at the bottom. Will keep thinking about it...

marick commented 13 years ago

I've been bashing against this much of the day, and I have one workaround:

  1. defrecord is a macro. Midje can override it, rewrite the forms given it, then call the original. Here's what the rewritten defrecord would look like:
(clojure.core/defrecord FooRecord [from-record]
  FooProtocol
  (strcat [this from-function]
          (if (oh_look_we_are_faked? behaviors.t-canary/strcat)
            (apply behaviors.t-canary/strcat [this from-function])
            ...original body...)))
  1. The ordinary behavior of provided can be unchanged. That just alter-var-root's the symbol naming the function to be a different function that records calls and provides faked values. So this works:
(fact
  (let [foo (FooRecord. "rec")]
    (foo-record-user foo) => "33"
    (provided (strcat foo "foo-record-user") => "33")))

This doesn't handle functions defined with extend-type etc, but this is slimy enough that I don't want to go too far. If we can succeed with defrecord, and get more people using network-of-facts-style testing, perhaps midje-ability might be a consideration for protocols going forward.

bmabey commented 13 years ago

You'd have to do the same for deftype as well...

marick commented 13 years ago

I think I have a workable implementation that sticks close to the Midje notion of describing relationships between functions. I've posted about it on the mailing list. If you're not on there, I can excerpt it.

On Jun 23, 2011, at 8:28 AM, stuarthalloway wrote:

It's just a matter of choosing the right thing to mock. Instead of mocking the function bar, you will need to mock the object FooRecord, e.g. using reify. The good news is that the approach should seem familiar to OO folks, who are always mocking the objects, not the fns. :-)

Since systems built around mock-based testing tend to expose plenty of injection points, I would expect this to be straightforward. If not, I would be happy to look at a larger example and propose ideas.

It is true that mocking plain fns and mocking objects will be two different things. That seems ok to me. Mocking is inherently a white-box exercise requiring that you deal with the specifics of your platform.

Reply to this email directly or view it on GitHub: https://github.com/marick/Midje/issues/17#issuecomment-1425170


Brian Marick, Artisanal Labrador Contract programming in Ruby and Clojure Occasional consulting on Agile www.exampler.com, www.twitter.com/marick

marick commented 13 years ago

General problem has solution, though there may still be unexplored corner cases.