Stubbing macro for isolated unit tests

57 views
Skip to first unread message

James Reeves

unread,
Nov 23, 2008, 11:24:22 AM11/23/08
to Clojure
In case anyone's interested, I've created a macro for stubbing
existing functions. I created it for my Fact unit testing library, but
it could be used with any unit testing framework, such as the test-is
library in clojure.contrib. Since stubbing is an important part of
isolating functions for the purpose of unit testing, it's possible
some people might find this useful:

(defn stubfn
"Given a map of argument vectors and return values, construct
a function to return the value associated with the key of
arguments."
[result-map]
(fn [& args] (result-map (vec args))))

(defmacro stub
"Create function stubs for isolated unit tests.
e.g. (stub [(f 1 2) 3
(f 3 2) 5]
(= (+ (f 1 2) (f 3 2))
8))"
[stubs & body]
(let [stub-pairs (partition 2 stubs)
make-maps (fn [[[f & args] ret]] {f {(vec args) ret}})
bind-stub (fn [[f clauses]] [f `(stubfn ~clauses)])]
`(binding
[~@(mapcat bind-stub
(apply merge-with merge
(map make-maps stub-pairs)))]
~@body)))

- James

Justin Giancola

unread,
Nov 23, 2008, 6:58:03 PM11/23/08
to Clojure
Neat. I noticed that you're forcing the arg lists into vectors in both
make-maps and in stubfn. Since they're not being manipulated at all,
you could just as easily leave them as seqs and everything will still
work.

It might be a bit clearer to use reduce instead of map and merge to
generate the stub maps, but YMMV.

(defmacro stub [stubs & body]
(let [stub-maps
(reduce (fn [acc [[f & args] res]]
(assoc-in acc [f args] res))
{}
(partition 2 stubs))]
`(binding
[~@(mapcat (fn [[fname fhash]]
`(~fname (fn [& args#] ('~fhash args#))))
stub-maps)]
~@body)))

I've also removed the helper function but on second thought it might
be nicer to keep it. If you delegate stubbed function calls to the
helper, it could be overridden to do something other than return nil
when stubbed functions are called with argument lists not specified in
the stub.


Justin

James Reeves

unread,
Nov 23, 2008, 9:56:02 PM11/23/08
to Clojure
On Nov 23, 11:58 pm, Justin Giancola <justin.gianc...@gmail.com>
wrote:
> Neat. I noticed that you're forcing the arg lists into vectors in both
> make-maps and in stubfn. Since they're not being manipulated at all,
> you could just as easily leave them as seqs and everything will still
> work.

The reduce is a good idea, but your method of quoting the hash doesn't
evaluate the return value:

user=> (stub [(f 1 2) (+ 1 2)] (f 1 2))
(+ 1 2)

Whilst with the original:

user=> (stub [(f 1 2) (+ 1 2)] (f 1 2))
3

This was why I turned it into a vector. It seemed the easiest way of
getting a hash that I didn't have to quote, but whose values would be
correctly evaluated.

- James

Justin Giancola

unread,
Nov 24, 2008, 1:19:10 PM11/24/08
to Clojure
On Nov 23, 9:56 pm, James Reeves <weavejes...@googlemail.com> wrote:
> On Nov 23, 11:58 pm, Justin Giancola <justin.gianc...@gmail.com>
> wrote:
>
> > Neat. I noticed that you're forcing the arg lists into vectors in both
> > make-maps and in stubfn. Since they're not being manipulated at all,
> > you could just as easily leave them as seqs and everything will still
> > work.
>
> The reduce is a good idea, but your method of quoting the hash doesn't
> evaluate the return value:
>
> user=> (stub [(f 1 2) (+ 1 2)] (f 1 2))
> (+ 1 2)
>
> Whilst with the original:
>
> user=> (stub [(f 1 2) (+ 1 2)] (f 1 2))
> 3

Oops--when I realized the arg list hash keys were being evaluated
during stub function creation I should have addressed that directly
instead of just quoting the entire hash!

> This was why I turned it into a vector. It seemed the easiest way of
> getting a hash that I didn't have to quote, but whose values would be
> correctly evaluated.

Using vectors will work since they're self-evaluating, but you can
also prevent the unwanted evaluation by wrapping them in quote forms
as the hash is being assembled:

(defmacro stub [stubs & body]
(let [stub-maps
(reduce (fn [acc [[f & args] res]]
(assoc-in acc [f `'~args] res))
{}
(partition 2 stubs))]
`(binding
[~@(mapcat (fn [[fname fhash]]
`(~fname (fn [& args#] (~fhash args#))))
stub-maps)]
~@body)))

This way when the stub functions are created you end up with list
literals for keys and correctly evaluated values.

However, quoting the arg lists this way necessitates that calls to the
stub functions have arg lists identical to those used to generate the
stubs.

i.e.
user=> (stub [(f 1 2) (+ 1 2)] (f 1 2))
3

but

user=> (stub [(f 1 (+ 1 1)) (+ 1 2)] (f 1 2))
nil

So, you need to evaluate all of the args independently when building
the list literals, possibly with:
(defmacro stub [stubs & body]
(let [stub-maps
(reduce (fn [acc [[f & args] res]]
(assoc-in acc [f `(list ~@args)] res))
{}
(partition 2 stubs))]
`(binding
[~@(mapcat (fn [[fname fhash]]
`(~fname (fn [& args#] (~fhash args#))))
stub-maps)]
~@body)))

I'm not sure this is quite as easy to follow, but I'd prefer to avoid
proxying all of the arg lists through vectors just because they're
self-evaluating.


Justin

James Reeves

unread,
Nov 27, 2008, 3:39:04 PM11/27/08
to Clojure


On Nov 24, 6:19 pm, Justin Giancola <justin.gianc...@gmail.com> wrote:
> So, you need to evaluate all of the args independently when building
> the list literals, possibly with:
> (defmacro stub [stubs & body]
>   (let [stub-maps
>         (reduce (fn [acc [[f & args] res]]
>                   (assoc-in acc [f `(list ~@args)] res))
>                 {}
>                 (partition 2 stubs))]
>     `(binding
>          [~@(mapcat (fn [[fname fhash]]
>                       `(~fname (fn [& args#] (~fhash args#))))
>                     stub-maps)]
>        ~@body)))
>
> I'm not sure this is quite as easy to follow, but I'd prefer to avoid
> proxying all of the arg lists through vectors just because they're
> self-evaluating.

Thanks for the suggested improvements! It took me a while to find the
time to code, but I've now integrated the improved stub macro into my
own code, with the lambdas bound to names via 'let' for clarity.

- James
Reply all
Reply to author
Forward
0 new messages