Spying and stubbing in Clojure and ClojureScript with test-doubles
Tags: clojure, opensource, webdev, testdoubles
Published: 2022-10-31 17:01:54

---
title: Spying and stubbing in Clojure and ClojureScript with test-doubles
published: true
description: "A small spying and stubbing library for Clojure and ClojureScript: test-doubles"
tags: 
- Clojure 
- OpenSource 
- webdev 
- TestDoubles

canonical_url: https://codesai.com/posts/2018/04/test-doubles-small-spying-and-stubbing

cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mtq1cx1k9s4a1mr2e3e4.png

# Use a ratio of 100:42 for best results.
# published_at: 2022-10-31 16:43 +0000
---

<div style="text-align: justify;">
As you may know from a <a href="https://www.codesai.com/2017/06/testing-om-components">previous post</a> I&#8217;m working for <a href="http://www.greenpowermonitor.com/">GreenPowerMonitor</a>
as part of a team that is developing a challenging SPA to monitor and manage renewable energy portfolios using ClojureScript.
</div>

<br>

<div style="text-align: justify;">
We were dealing with some legacy code that was effectful and needed to be tested using <a href="http://xunitpatterns.com/Test%20Double.html">test doubles</a>, 
so we explored some existing ClojureScript libraries but we didn't feel comfortable with them. On one hand, we found that some of them had different macros for different types of test doubles and this made tests that needed both spies and stubs become very nested. We wanted to produce tests with as little nesting as possible. On the other hand, being used to <a href="https://martinfowler.com/bliki/TestDouble.html">Gerard Meszaros&#8217; vocabulary for tests doubles</a>, we found the naming used for different types of tests doubles in some of the existing libraries a bit confusing. We wanted to stick to <a href="https://martinfowler.com/bliki/TestDouble.html">Gerard Meszaros&#8217; vocabulary for tests doubles</a>. 
</div>

<br>

<div style="text-align: justify;">
So we decided we'd write our own <a href="http://xunitpatterns.com/Test%20Stub.html">stubs</a> and <a href="http://xunitpatterns.com/Test%20Spy.html">spies</a> library. 
</div>

<br>

<div style="text-align: justify;">
We started by manually creating our own spies and stubs during some time so that we could identify the different ways in which we were going to use them. After a while, my colleague <a href="https://github.com/andrestylianos">André Stylianos Ramos</a> and I wrote our own small DSL to create stubs and spies using macros to remove all that duplication and boiler plate. The result was a small library that we've been using in our ClojureScript project for nearly a year and that we've recently adapted to make it work in Clojure as well: 
</div>

<br>

```clojure
(ns greenpowermonitor.test-doubles
  #?(:cljs (:require-macros
             [greenpowermonitor.test-doubles])))

(def ^:dynamic *spies-atom* (atom {}))

(defn make-spy-fn []
  (let [func-atom (atom [])]
    (letfn [(spy-fn [& args]
              (swap! func-atom conj (vec args)))]
      (swap! *spies-atom* conj {spy-fn func-atom})
      spy-fn)))

(defn make-mult-calls-stub-fn [values]
  (let [values-atom (atom values)]
    (fn [& _]
      (if (empty? @values-atom)
        (throw (ex-info "Too many calls to stub"
                        {:causes :calls-exceeded
                         :provided-return-values values}))
        (let [value (first @values-atom)]
          (swap! values-atom rest)
          value)))))

(defn make-stub-fn [option args]
  (case option
    :maps (fn [& fn-args]
            (let [call-args (vec fn-args)]
              (get args call-args (:any args))))
    :returns (make-mult-calls-stub-fn args)
    :constantly (constantly args)
    (throw (ex-info "Using :stubbing with an unknown option"
                    {:cause :unknown-option
                     :used-option option
                     :available-options [:maps :returns :constantly]}))))

#?(:clj
   (defn- create-spying-list [functions]
     (mapcat #(vector % `(make-spy-fn)) functions)))

#?(:clj
   (defn- create-stubbing-list [functions]
     (->> functions
          (partition 3)
          (mapcat (fn [[func key values]] [func `(make-stub-fn ~key ~values)])))))

#?(:clj
   (defn- create-ignoring-list [functions]
     (mapcat #(vector % `(constantly nil)) functions)))

#?(:clj
   (defn- create-doubles-list [spying stubbing ignoring]
     (vec (concat (create-spying-list spying)
                  (create-stubbing-list stubbing)
                  (create-ignoring-list ignoring)))))

#?(:clj
   (defn- extract-with-double-args [args]
     (let [double-def? #(or (vector? %) (keyword? %))]
       (split-with double-def? args))))

#?(:clj
   (defmacro with-doubles [& args]
     (let [[doubles body] (extract-with-double-args args)
           {:keys [spying stubbing ignoring]
            :or {spying [] stubbing [] ignoring []}} doubles]
       `(with-redefs ~(create-doubles-list spying stubbing ignoring)
          ~@body
          (reset! *spies-atom* {})))))

(defn calls-to [function]
  (if-let [calls (some-> *spies-atom* deref (get function) deref)]
    calls
    (let [error-msg "Attempting to check calls for a function that is not being spied on"]
      (throw #?(:clj  (Exception. error-msg)
                :cljs (js/Error. error-msg))))))
```

<div style="text-align: justify;">
I&#8217;m really glad to announce that <a href="http://www.greenpowermonitor.com/">GreenPowerMonitor</a> has open-sourced our small spying and stubbing library for Clojure and ClojureScript: <a href="https://github.com/GreenPowerMonitor/test-doubles">test-doubles</a>.
</div>

<br>

<div style="text-align: justify;">
In the following example written in ClojureScript, we show how we are using <code>test-doubles</code> to create two <a href="https://github.com/GreenPowerMonitor/test-doubles/blob/master/README.md#1-stubbing-function-calls">stubs</a> (one with the <a href="https://github.com/GreenPowerMonitor/test-doubles/blob/master/README.md#13-maps">:maps option</a> and another with the <a href="https://github.com/GreenPowerMonitor/test-doubles/blob/master/README.md#11-returns-option">:returns option</a>) and a <a href="https://github.com/GreenPowerMonitor/test-doubles/blob/master/README.md#2-spying-function-calls">spy</a>:
</div>

<br>

```clojure
(ns greenpowermonitor.test-doubles.example
  (:require
   [clojure.test :refer [deftest testing is]]
   [greenpowermonitor.test-doubles :as td] 
   [horizon.common.ajax.api :as service]
   [horizon.common.config :as c]
   [horizon.common.state.lens :as l]
   [horizon.domain.maintenance.work-orders.member :as sut]
   [horizon.domain.rim :as domain.rim]))

(deftest saving-changes
  (let [some-api-url "some-url"]
    (td/with-doubles
      :stubbing [l/view :maps {[domain.rim/rim-wo-edit-changes-lens] {:interventions {53 7}}
                               [domain.rim/rim-wo-edit-state-id-lens] 1
                               [domain.rim/rim-wo-translated-lens] {:interventions [{:value 1
                                                                                     :state-id 1
                                                                                     :id 53}]}}
                 c/mk-work-orders-save-url :returns [some-api-url]]
      :spying [service/put]

      (sut/save-changes!)

      (is (= 1 (-> service/put td/calls-to count)))   
      (let [[url data] (-> service/put td/calls-to first)]
        (is (= some-api-url url))
        (is (= {:state-id 1
                :id 3
                :values [{:id 53 :value "7"}]}
               (:json-params data)))))))
```

<div style="text-align: justify;">
We could show you more examples here of how <code>test-doubles</code> can be used and the different options it provides, but we&#8217;ve already included a lot of explained examples in its <a href="https://github.com/GreenPowerMonitor/test-doubles/blob/master/README.md">documentation</a>.
</div>

<br>

<div style="text-align: justify;">
Please do have a look and try our library. You can get its <a href="https://clojars.org/greenpowermonitor/test-doubles">last version from Clojars</a>. We hope it might be as useful to you as it has been for us.
</div>