Sunday, 28 October 2012

Easy mocking with Clojure [tech]

Attention conservation notice: Technical post of interest to programmers only. If you just want the link to my mocking system for Clojure, click it.

I hate testing.

I know that when I have a computer checking and re-checking the behaviour of my code, it's more reliable. I know that I can make changes more confidently, knowing that if I accidentally break something, it will probably get caught. I know all this, and yet I am still put off by the sheer tedium of building all the scaffolding any non-trivial piece of code usually needs.

So here's a library to make it easier, in my current favourite language.

You specify a sequence of function calls that your test will perform. The library mocks them out, and verifies that they are called in the right order with the right parameters (specified with destructuring pattern matching from clojure.core.match, so you can specify them as loosely or as tightly as you like).

You can add extra keywords to:
  • Fail the test if a certain function is called (:never)
  • Allow any number of extra calls to a certain function, provided they match a pattern (:more)
  • Fall through to the original function after executing the mock (:do).
You can put any code you like in the mock functions, so you can write additional tests or compute return values.

Example

I end up writing code like this a lot, when I'm dealing with hardware:

(defn launch-rocket []

  (when-let [key (launch-key-present?)]

    (start-fuel-pump)

    (while (< (get-fuel-pressure) 1000.0)
      (Thread/yield))

    (ignition/enable-with-key key)

    (when (< (get-fuel-pressure) 900.0)
      (abort!))

    (send-email "mission-control@example.com"
      (str "We have liftoff at " (java.util.Date.))))) 


Now, writing test cases for code like this is easy:

(deftest successful-launch
  (with-expect-call
      [(launch-key-present? [] "secret key")
       (start-fuel-pump)
       ; Check it doesn't go anywhere until it has a good
       ; pressure reading
       (get-fuel-pressure [] 860.0)
       (get-fuel-pressure [] 1001.0)
       ; After this, we don't care how many more times it
       ; checks the pressure
       (:more get-fuel-pressure [] 950.0)
       ; ...but it definitely fails the test if it aborts
       ; on this input data.
       (:never abort!)
       ; Does it use the right key?
       (ignition/enable-with-key ["secret key"])
       ; Use a binding to capture and examine the email body
       (send-email ["mission-control@example.com" body]
         (assert (re-matches
                   #"We have liftoff at .* ..:..:.. 20.."
                   body)))]
    (launch-rocket))


Wasn't that more fun than dependency injection?

The code's on GitHub – go try it out!

No comments:

Post a Comment