Chapter 10. Test-driven development and more

This chapter covers

  • An introduction to unit testing Clojure
  • Writing test-driven Clojure code
  • Mocking and stubbing code in Clojure
  • Improving test organization

Test-driven development (TDD) has become something of the norm on most software development projects. It’s easy to understand why, because TDD has several advantages. It allows the programmer to look at code being developed from the point of view of a consumer, which results in a more useful design when compared with a library that might be designed in relative isolation. Further, because code developed using TDD must be testable (by definition), the resulting design is often better in terms of lower coupling as well. Finally, the suite of tests that results from the process is a good way to ensure that functionality doesn’t regress in the face of enhancements and bug fixes.

Clojure has excellent support for unit testing. Further, because Clojure is a Lisp, it’s also extremely well suited for rapid application development. The read-evaluate-print loop (REPL) supports this by offering a means to develop code in an incremental manner. In this chapter, as you learn about TDD, you’ll use the REPL for quick experiments and such. As you’ll discover, this combination of TDD and the REPL makes for a productive development environment. The specific unit-testing library we’ll explore is called clojure.test, and it comes as a standard part of the Clojure distribution. Finally, we’ll look into mocking and stubbing needs that you might run into, and you’ll write code to handle such situations.

10.1. Getting started with TDD: Manipulating dates in strings

In this section, you’ll develop some code in a test-first manner. The first example is a set of functions that help with strings that represent dates. Specifically, you’ll write functions to increment and decrement such date strings. Such operations are often needed in many applications, so this functionality may prove useful as a utility. Although this example is simple, it illustrates the technique of writing unit tests and then getting them to pass, while also using the REPL to make the process quicker.

In TDD, you begin by writing a test. Obviously, because no code exists to support the test, it will fail. Making that failing test pass becomes the immediate goal, and this process repeats. So the first thing you’ll need is a test, which you’ll start writing next.

In this simple example, the test you’ll write is for a function that can accept a string containing a date in a particular format, and you’ll check to see if you can access its components.

10.1.1. First assertion

In this initial version of the test, you’ll check that the day portion is correct. Consider the following code (remember to put it in a file called date_operations_spec.clj in a folder named clj_in_act/ch10 within your source directory):

(ns clj-in-act.ch10.date-operations-spec
  (:require [clojure.test :refer :all]
            [clj-in-act.ch10.date-operations :refer :all]))
(deftest test-simple-data-parsing
  (let [d (date "2009-01-22")]
    (is (= (day-from d) 22))))

You’re using the clojure.test unit-testing library for Clojure, which began life as an independent project and was later included as part of the distribution. There are other open source unit-testing libraries for Clojure (such as Midje (https://github.com/marick/Midje), expectations (http://jayfields.com/expectations/), and others), but for most purposes, the basic clojure.test is sufficient. The first evidence that you’re looking at a unit test is the use of the deftest macro. Here’s the general form of this macro:

(deftest [name & body])

It looks somewhat like a function definition, without any parameters. The body here represents the code that will run when the unit test is executed. The clojure.test library provides a couple of assertion macros, the first being is, which was used in the previous example. You’ll see the use of the other macro in the following paragraphs.

Meanwhile, let’s return to the test. If you try to evaluate the test code at the REPL, Clojure will complain that it can’t find the clj-in-act.ch10.date-operations namespace. The error might look something like the following:

FileNotFoundException Could not locate clj_in_act/ch10/date_operations __init.class or clj_in_act/ch10/date_operations.clj on classpath:   clojure.lang.RT.load (RT.java:443)

To move past this error, create a new namespace in an appropriately located file. This namespace has no code in it, so your test code still won’t evaluate, but the error will be different. It will complain that it’s unable to find the definition of a function named date:

CompilerException java.lang.RuntimeException: No such var: clj-in-act.ch10.date-operations/date, compiling:(NO_SOURCE_PATH:1:1)

Getting past this error is easy; define a date function in your new date-operations namespace. To begin with, it doesn’t even have to return anything. The same goes for the day-from function:

(ns clj-in-act.ch10.date-operations)
(defn date [date-string])
(defn day-from [d])

This will cause your test to evaluate successfully, leaving it ready to be run. You can also do this from the REPL, like so:

(use 'clojure.test)
;=> nil
(run-tests 'clj-in-act.ch10.date-operations-spec)
Testing clj-in-act.ch10.date-operations-spec
FAIL in (test-simple-data-parsing) (NO_SOURCE_FILE:1)
expected: (= (day-from d) 22)
  actual: (not (= nil 22))
Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
;=> {:type :summary, :test 1, :pass 0, :fail 1, :error 0}

Now you’re set. You have a failing test that you can work on, and once you have it passing, you’ll have the basics of what you want. To get this test to pass, you’ll write some real code in the clj-in-act.ch10.date-operations namespace. One way to implement this functionality is to use classes from the JDK standard library (there are other options as well, such as the excellent Joda Time library available as open source). You’ll stick with the standard library, specifically with the GregorianCalendar and the SimpleDateFormat classes. You can use these to convert strings into dates. You can experiment with them on the REPL:

(import '(java.text SimpleDateFormat))
;=> java.text.SimpleDateFormat
(def f (SimpleDateFormat. "yyyy-MM-dd"))
;=> #'user/f
(.parse f "2010-08-15")
;=> #inst "2010-08-15T05:00:00.000-00:00"

So you know SimpleDateFormat will work, and now you can check out the GregorianCalendar:

(import '(java.util GregorianCalendar))
;=> java.util.GregorianCalendar
(def gc (GregorianCalendar.))
;=> #'user/gc

Now that you have an instance of GregorianCalendar in hand, you can set the time by parsing a date string and then calling setTime:

(def d (.parse f "2010-08-15"))
;=> #'user/d
(.setTime gc d)
;=> nil

Because setTime returns nil, you’re going to have to explicitly pass back the GregorianCalendar object. Once you’ve performed this experiment, you can write the code, which ends up looking like this:

(ns clj-in-act.ch10.date-operations
  (:import (java.text SimpleDateFormat)
           (java.util Calendar GregorianCalendar)))
(defn date [date-string]
  (let [f (SimpleDateFormat. "yyyy-MM-dd")
        d (.parse f date-string)]
    (doto (GregorianCalendar.)
      (.setTime d))))
;=> #'clj-in-act.ch10.date-operations/date
(date "2010-08-15")
;=> #inst "2010-08-15T00:00:00.000-05:00"

Also, you have to figure out the implementation of day-from. A look at the API documentation for GregorianCalendar reveals that the get method is what you need. You can try it at the REPL:

(import '(java.util Calendar))
;=> java.util.Calendar
(.get gc Calendar/DAY_OF_MONTH)
;=> 15

Again, you’re all set. The day-from function can be

(defn day-from [d]
  (.get d Calendar/DAY_OF_MONTH))

The test should pass now. Remember that for the REPL to see the new definitions of the code in the date-operations namespace, you may need to reload it (using the :reload option). Here’s the output:

(run-tests 'clj-in-act.ch10.date-operations-spec)
Testing clj-in-act.ch10.date-operations-spec
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
;=> {:type :summary, :test 1, :pass 1, :fail 0, :error 0}

Now that you can create date objects (represented by instances of GregorianCalendar) and can access the day from these objects, you can implement accessors for month and year. Again, you’ll begin with writing a test.

10.1.2. month-from and year-from

The test for getting the month and year is similar to what you wrote before. You can include these assertions in the previous test:

(deftest test-simple-data-parsing
  (let [d (date "2009-01-22")]
    (is (= (month-from d) 1))
    (is (= (day-from d) 22))
    (is (= (year-from d) 2009))))

This won’t evaluate until you at least define the month-from and year-from functions. You’ll skip over the empty functions and write the implementation as

(defn month-from [d]
  (inc (.get d Calendar/MONTH)))
(defn year-from [d]
  (.get d Calendar/YEAR))

With this code in place, the test should pass:

(run-tests 'clj-in-act.ch10.date-operations-spec)
Testing clj-in-act.ch10.date-operations-spec
Ran 1 tests containing 3 assertions.
0 failures, 0 errors.
;=> {:type :summary, :test 1, :pass 3, :fail 0, :error 0}

Again, you’re ready to add more features to your little library. You’ll add an as-string function that can convert your date objects into the string format.

10.1.3. as-string

The test for this function is quite straightforward, because it’s the same format you began with:

(deftest test-as-string
  (let [d (date "2009-01-22")]
    (is (= (as-string d) "2009-01-22"))))

Because you have functions to get the day, month, and year from a given date object, it’s trivial to write a function that constructs a string containing words separated by dashes. Here’s the implementation, which will compile and run after you include clojure.string in the namespace via a require clause:

(require '[clojure.string :as str])
(defn as-string [date]
  (let [y (year-from date)
        m (month-from date)
        d (day-from date)]
    (str/join "-" [y m d])))

You can confirm that this works by running it at the REPL:

(def d (clj-in-act.ch10.date-operations/date "2010-12-25"))
;=> #'user/d
(as-string d)
;=> "2010-12-25"

So that works, which means your test should pass. Running the test now gives the following output:

(run-tests 'clj-in-act.ch10.date-operations-spec)
Testing clj-in-act.ch10.date-operations-spec
FAIL in (test-as-string) (NO_SOURCE_FILE:1)
expected: (= (as-string d) "2009-01-22")
  actual: (not (= "2009-1-22" "2009-01-22"))
Ran 2 tests containing 4 assertions.
1 failures, 0 errors.
;=> {:type :summary, :test 2, :pass 3, :fail 1, :error 0}

The test failed! The problem is that instead of returning "2009-01-22", your as-string function returns "2009-1-22", because the various parts of the date are returned as numbers without leading zeroes even when they consist of only a single digit. You’ll either have to change your test (which is fine, depending on the problem at hand) or pad such numbers to get your test to pass. For this example, you’ll do the latter:

(defn pad [n]
  (if (< n 10) (str "0" n) (str n)))
(defn as-string [date]
  (let [y (year-from date)
        m (pad (month-from date))
        d (pad (day-from date))]
    (str/join "-" [y m d])))

Running the test now should show a better response:

(run-tests 'clj-in-act.ch10.date-operations-spec)
Testing clj-in-act.ch10.date-operations-spec
Ran 2 tests containing 4 assertions.
0 failures, 0 errors.
;=> {:type :summary, :test 2, :pass 4, :fail 0, :error 0}

So, you now have the ability to create date objects from strings, get at parts of the dates, and also convert the date objects into strings. You can either continue to add features or take a breather to refactor your code a little.

10.1.4. Incrementing and decrementing

Because you’re just getting started, we’ll postpone refactoring until after adding one more feature: adding functionality to advance and turn back dates. You’ll start with addition, and then you’ll write a test:

(deftest test-incrementing-date
  (let [d (date "2009-10-31")
        n-day (increment-day d)]
    (is (= (as-string n-day) "2009-11-01"))))

This test will fail, citing the inability to find the definition of increment-day. You can implement this function using the add method on the GregorianCalendar class, which you can check on the REPL:

(def d (date "2009-10-31"))
;=> #'user/d
(.add d Calendar/DAY_OF_MONTH 1)
;=> nil
(as-string d)
;=> "2009-11-01"

So that works quite nicely, and you can convert this into a function, as follows:

(defn increment-day [d]
  (doto d
    (.add Calendar/DAY_OF_MONTH 1)))

Now, you can add a couple more assertions to ensure you can add not only days but also months and years. The modified test looks like this:

(deftest test-incrementing-date
  (let [d (date "2009-10-31")
        n-day (increment-day d)
        n-month (increment-month d)
        n-year (increment-year d)]
    (is (= (as-string n-day) "2009-11-01"))
    (is (= (as-string n-month) "2009-11-30"))
    (is (= (as-string n-year) "2010-10-31"))))

The code to satisfy this test is simple, now that you already have increment-day:

(defn increment-month [d]
  (doto d
    (.add Calendar/MONTH 1)))
(defn increment-year [d]
  (doto d
    (.add Calendar/YEAR 1)))

Running this results in the following output:

(run-tests 'clj-in-act.ch10.date-operations-spec)
Testing clj-in-act.ch10.date-operations-spec
FAIL in (test-incrementing-date) (NO_SOURCE_FILE:1)
expected: (= (as-string n-day) "2009-11-01")
  actual: (not (= "2010-12-01" "2009-11-01"))
FAIL in (test-incrementing-date) (NO_SOURCE_FILE:1)
expected: (= (as-string n-month) "2009-11-30")
  actual: (not (= "2010-12-01" "2009-11-30"))
FAIL in (test-incrementing-date) (NO_SOURCE_FILE:1)
expected: (= (as-string n-year) "2010-10-31")
  actual: (not (= "2010-12-01" "2010-10-31"))
Ran 4 tests containing 8 assertions.
3 failures, 0 errors.
;=> {:type :summary, :test 4, :pass 5, :fail 3, :error 0}

All the tests failed! Even the one that was passing earlier (incrementing the date by a day) is now failing. Looking closely, all three failures are because the incremented date seems to be "2010-12-01". It appears that "2009-10-31" was incremented first by a day, then by a month, and then by a year! You’ve been bitten by the most-Java-objects-are-not-immutable problem. Because d is a mutable object, and you’re calling increment-day, increment-month, and increment-year on it, you’re accumulating the mutations, resulting in a final date of "2010-12-01". (As a side note, this also illustrates how easy it is to get used to Clojure’s immutability and then to expect everything to behave like Clojure’s core data structures. Within a few days of using Clojure, you’ll begin to wonder why you ever thought mutable objects were a good idea!)

To address this problem, you’ll return a new date from each mutator function. The clone method in Java does this, and you can use it in your new definitions:

(defn increment-day [d]
  (doto (.clone d)
    (.add Calendar/DAY_OF_MONTH 1)))
(defn increment-month [d]
  (doto (.clone d)
    (.add Calendar/MONTH 1)))
(defn increment-year [d]
  (doto (.clone d)
    (.add Calendar/YEAR 1)))

With this change, all the tests pass, allowing us to now tackle decrementing. Again, you’ll start with a test:

(deftest test-decrementing-date
  (let [d (date "2009-11-01")
        n-day (decrement-day d)
        n-month (decrement-month d)
        n-year (decrement-year d)]
    (is (= (as-string n-day) "2009-10-31"))
    (is (= (as-string n-month) "2009-10-01"))
    (is (= (as-string n-year) "2008-11-01"))))

To get this test to pass, you can go with the same structure of functions that did the incrementing. The code might look like the following:

(defn decrement-day [d]
  (doto (.clone d)
    (.add Calendar/DAY_OF_MONTH -1)))
(defn decrement-month [d]
  (doto (.clone d)
    (.add Calendar/MONTH -1)))
(defn decrement-year [d]
  (doto (.clone d)
    (.add Calendar/YEAR -1)))

Each function calls an appropriate Java method, and this passes all the tests. You now have code that works and a library that can accept date strings and return dates as strings. It can also increment and decrement dates by days, months, and years. But the code isn’t quite optimal, so you’re now going to improve it.

10.1.5. Refactor mercilessly

Extreme programming (XP) is an Agile methodology that espouses several specific guidelines. One of them is to “refactor mercilessly.” It means that you should continuously strive to make code (and design) simpler by removing clutter and needless complexity. An important part of achieving such simplicity is to remove duplication. You’ll do that with the code you’ve written so far.

Before you start, it’s pertinent to make an observation. There’s one major requirement to any sort of refactoring: for it to be safe, there needs to be a set of tests that can verify that nothing broke because of the refactoring. This is another benefit of writing tests (and TDD in general). The tests from the previous section will serve this purpose.

You’ll begin refactoring by addressing the duplication in the increment/decrement functions. Here’s a rewrite of those functions:

(defn date-operator [operation field]
  (fn [d]
    (doto (.clone d)
      (.add field (operation 1)))))
(def increment-day (date-operator  + Calendar/DAY_OF_MONTH))
(def increment-month (date-operator + Calendar/MONTH))

(def increment-year (date-operator + Calendar/YEAR))
(def decrement-day (date-operator - Calendar/DAY_OF_MONTH))
(def decrement-month (date-operator - Calendar/MONTH))
(def decrement-year (date-operator - Calendar/YEAR))

After replacing all six of the old functions with this code, the tests still pass. You’ve removed the duplication from the previous implementation and also made the code more declarative: the job of each of the six functions is clearer with this style. The benefit may seem small in this example, but for more complex code, it can be a major boost in readability, understandability, and maintainability. This refactored version can be further reduced via some clever use of convention, but it may be overkill for this particular task. As it stands, you’ve reduced the number of lines from 18 to 10, showing that the old implementation was a good 80% larger than this new one.

Imagine a similar refactoring being applied to the month-from, day-from, and year-from functions. What might that look like?

This section showed how to use the built-in Clojure unit-testing library called clojure.test. As you saw through the course of building the example, using the REPL is a critical element to writing Clojure code. You can use the REPL to quickly check how things work and then write code once you understand the APIs. It’s great for such short experiments and allows for incrementally building up code for larger, more complex functions. When a unit-testing library is used alongside the REPL, the combination can result in an ultrafast development cycle while keeping quality high. In the next section, you’ll see how you can write a simple mocking and stubbing library to make your unit testing even more effective.

10.2. Improving tests through mocking and stubbing

Unit testing is testing at a unit level, which in the case of Clojure is the function. Functions are often composed of other functions, and there are times when testing such upper-level functions that it’s useful to mock out calls to certain underlying functions. Mocking functions is a useful technique (often used during unit testing) where a particular function is replaced with one that doesn’t do anything. This allows you to focus only on those parts of the code where the unit test is being targeted.

At other times, it’s useful to stub the calling of a function, so instead of doing what it’s implemented to do, the stubbed function returns canned data.

You’ll see examples of both of these in this section. You’ll also write a simple library to handle mocking and stubbing functions in this manner. Clojure, being the dynamic functional language that it is, makes this extremely easy to do.

10.2.1. Example: Expense finders

In this example, you’ll write a few functions to load certain expense records from a data store and then filter them based on some criteria (such as greater than a particular amount). You might do this as part of an expense report builder, for instance. Because you’re dealing with money, you’ll also throw in a requirement that your functions must log to an audit log.

Also, the focus of this section isn’t the TDD that you saw in the previous section. This section will focus on the need to stub calls to certain functions. The following listing shows the code you’re trying to test.

Listing 10.1. Example code that fetches and filters expenses from a data store
(ns clj-in-act.ch10.expense-finders
  (:require [clojure.string :as str]))
(defn log-call [id & args]
  (println "Audit - called" id "with:" (str/join ", " args))
  ;;do logging to some audit data-store
)
(defn fetch-all-expenses [username start-date end-date]
  (log-call "fetch-all" username start-date end-date)
  ;find in data-store, return list of expense maps
)
(defn expenses-greater-than [expenses threshold]
  (log-call "expenses-greater-than" threshold)
  (filter #(> (:amount %) threshold) expenses))
(defn fetch-expenses-greater-than [username start-date end-date threshold]
  (let [all (fetch-all-expenses username start-date end-date)]
    (expenses-greater-than all threshold)))

Here again, expense records are represented as Clojure maps. The log-call function presumably logs calls to some kind of an audit database. The two fetch functions both depend on loading expenses from some sort of data store. To write a test for, say, the fetch-expenses-greater-than function, you’ll need to populate the data store to ensure it’s loaded from the test via the fetch-all-expenses call. In case any test alters the data, you must clean it up so subsequent runs of the tests also work.

This is a lot of trouble. Moreover, it couples your tests to the data store and the data in it. Presumably, as part of a real-world application, you’d test the persistence of data to and from the data store elsewhere, so having to deal with hitting the data store in this test is a distraction and plain unnecessary. It would be nice if you could stub the call and return canned data. You’ll implement this stubbing functionality next. Further, you’ll look at dealing with another distraction, the log-call function, in the following section.

10.2.2. Stubbing

In your test for fetch-expenses-greater-than, it would be nice if you could do the following:

(let [filtered (fetch-expenses-greater-than "" "" "" 15.0)]
  (is (= (count filtered) 2))
  (is (= (:amount (first filtered)) 20.0))
  (is (= (:amount (last filtered)) 30.0)))

You’re passing blank strings to fetch-expenses-greater-than because you don’t care what the values are (you could have passed anything). Inside the body of fetch-expenses-greater-than, they’re used only as arguments to fetch-all-expenses, and you want to stub the call to this latter function (the one parameter that you do pass correctly is the last one, with a value of 15.0). What you’d also like is for the stubbed call to return canned data, which you might define as follows:

(def all-expenses [{:amount 10.0 :date "2010-02-28"}
                   {:amount 20.0 :date "2010-02-25"}
                   {:amount 30.0 :date "2010-02-21"}])

So, the question is how do you express the requirement for these two things: the call to fetch-all-expenses is faked out (stubbed), and it returns all-expenses?

Stubbing macro

To make the process of stubbing functions feel as natural as possible, you’ll create a new construct for your tests and give it the original name stubbing. After you have it all implemented, you’ll be able to say something like this:

(deftest test-fetch-expenses-greater-than
  (stubbing [fetch-all-expenses all-expenses]
    (let [filtered (fetch-expenses-greater-than "" "" "" 15.0)]
      (is (= (count filtered) 2))
      (is (= (:amount (first filtered)) 20.0))
      (is (= (:amount (last filtered)) 30.0)))))

The general form of the stubbing macro is as follows:

(stubbing [function-name1 stubbed-return-value1
           function-name2 stubbed-return-value2 ...]
    code-body)

This reads a little like the let and binding forms, and whenever you add such constructs to your code, it makes sense to make them look and feel like one of the built-in features of Clojure to keep things easy for others to understand. Now let’s see how you might implement it.

Implementing stubbing

Clojure makes implementing this quite easy. Because it’s a functional language, you can easily create a dummy function on the fly, one that accepts an arbitrary number of parameters and returns whatever you specify. Next, because function definitions are held in vars, you can then use the binding form to set them to your newly constructed stub functions. Here’s the implementation:

(ns clj-in-act.ch10.stubbing)
(defmacro stubbing [stub-forms & body]
  (let [stub-pairs (partition 2 stub-forms)
        returns (map last stub-pairs)
        stub-fns (map #(list 'constantly %) returns)
        real-fns (map first stub-pairs)]

    `(with-redefs [~@(interleave real-fns stub-fns)]
       ~@body)))

Considering that many languages have large, complex libraries for stubbing functions and methods, this code is almost disappointingly short!

Before we look at a sample expansion of this macro, let’s look at an example of two functions, calc-x and calc-y, being called from some client code:

(defn calc-x [x1 x2]
  (* x1 x2))
(defn calc-y [y1 y2]
   (/ y2 y1))
(defn some-client []
  (println (calc-x 2 3) (calc-y 3 4)))

Let’s see how some-client behaves under normal conditions:

(some-client)
6 4/3
;=> nil

And here’s how it behaves using the new stubbing macro:

(stubbing [calc-x 1
           calc-y 2]
  (some-client))
1 2
;=> nil

So now that we’ve confirmed that this works as expected, let’s look at how it does so:

(macroexpand-1' (stubbing [calc-x 1 calc-y 2]
        (some-client)))
;=> (clojure.core/with-redefs [calc-x (constantly 1)
                               calc-y (constantly 2)]
      (some-client))

The constantly function does the job well, but to make things easier for you later on, you’ll introduce a function called stub-fn. It’s a simple higher-order function that accepts a value and returns a function that returns that value no matter what arguments it’s called with. Hence, it’s equivalent to constantly. The rewritten code is shown here:

This extra layer of indirection will allow you to introduce another desirable feature into this little library (if you can even call it that!)—mocking, the focus of the next section.

10.2.3. Mocking

Let’s begin by going back to what you were doing when you started the stubbing journey. You wrote a test for fetch-expenses-greater-than, a function that calls expenses-greater-than. This function does two things: it logs to the audit log, and then it filters out the expenses based on the threshold parameter. You should be unit testing this lower-level function as well, so let’s look at the following test:

(ns clj-in-act.ch10.expense-finders-spec
  (:require [clj-in-act.ch10.expense-finders :refer :all]
            [clojure.test :refer :all]))
(deftest test-filter-greater-than
  (let [fetched [{:amount 10.0 :date "2010-02-28"}
                 {:amount 20.0 :date "2010-02-25"}
                 {:amount 30.0 :date "2010-02-21"}]
        filtered (expenses-greater-than fetched 15.0)]
    (is (= (count filtered) 2))
    (is (= (:amount (first filtered)) 20.0))
    (is (= (:amount (last filtered)) 30.0))))

Running the test gives the following output:

(run-tests 'clj-in-act.ch10.expense-finders-spec)
Testing clj-in-act.ch10.expense-finders-spec
Audit - called expenses-greater-than with: 15.0
Ran 1 tests containing 3 assertions.
0 failures, 0 errors.
;=> {:type :summary, :test 1, :pass 3, :fail 0, :error 0}

It works, and the test passes. The trouble is that the audit function also runs as part of the test, as can be seen from the text Audit - called expenses-greater-than with: 15.0 that was printed by the log-call function. In the present case, all it does is print some text, but in the real world, it could do something useful—perhaps write to a database or send a message on a queue.

Ultimately, it causes the tests to be dependent on an external system such as a database server or a message bus. It makes the tests less isolated, and it detracts from the unit test itself, which is trying to check whether the filtering works correctly.

One solution is to not test at this level at all but to write an even lower-level function that tests only the filtering. But you’d like to test at least at the level that clients of the code will work at, so you need a different solution. One approach is to add code to the log-call function so that it doesn’t do anything when running in test mode. But that adds unnecessary code to functions that will run in production, and it also clutters the code. In more complex cases, it will add noise that will detract from easily understanding what the function does.

Luckily, you can easily fix this problem in Clojure by writing a simple mocking library.

10.2.4. Mocks versus stubs

A mock is similar to a stub because the original function doesn’t get called when a function is mocked out. A stub returns a canned value that was set up when the stub was set up. A mock records the fact that it was called, with a specific set of arguments. Later on, the developer can programmatically verify if the mocked function was called, how many times it was called, and with what arguments.

Creating mocks with stubs

Now that you have a separate function called stub-fn, you can modify this to add mocking capabilities. You’ll begin by creating an atom called mock-calls that will hold information about the various mocked functions that were called:

(def mock-calls (atom {}))

Now, you’ll modify stub-fn to use this atom:

(defn stub-fn [the-function return-value]
  (swap! mock-calls assoc the-function [])
  (fn [& args]
    (swap! mock-calls update-in [the-function] conj args)
    return-value))

When stub-fn is called, an empty vector is stored in the atom against the function being stubbed. Later, when the stub is called, it records the call in the atom (as shown in chapter 6) along with the arguments it was called with. It then returns the return-value it was created with, thereby working as before in that respect.

Now that you’ve changed the way stub-fn works, you have to also slightly refactor the stubbing macro for it to stay compatible:

(defmacro stubbing [stub-forms & body]
  (let [stub-pairs (partition 2 stub-forms)
        real-fns (map first stub-pairs)
        returns (map last stub-pairs)
        stub-fns (map #(list `stub-fn %1 %2) real-fns returns)]
    `(with-redefs [~@(interleave real-fns stub-fns)]
       ~@body)))

Okay, now you’ve laid the basic foundation on which to implement the mocking features. Because a mock is similar to a stub, you can use stub-fn to create a new one. You don’t care about a return value, so you’ll use nil:

(defn mock-fn [the-function]
  (stub-fn the-function nil))

Now for some syntactic sugar. You’ll create a new macro called mocking, which will behave similar to stubbing, except that it will accept any number of functions that need to be mocked:

(defmacro mocking [fn-names & body]
  (let [mocks (map #(list `mock-fn (keyword %)) fn-names)]
    `(with-redefs [~@(interleave fn-names mocks)]
       ~@body)))

Now that you have the basics ready, you can rewrite your test:

When you run this test, it won’t execute the log-call function, and the test is now independent of the whole audit-logging component. As noted earlier, the difference between mocking and stubbing so far is that you don’t need to provide a return value when using mocking.

Although you don’t want the log-call function to run as is, it may be important to verify that the code under test calls a function by that name. Perhaps such calls are part of some security protocol in the overall application. It’s quite easy for you to verify this, because you’re recording all calls to your mocked functions in the mock-calls atom.

Verifying mocked calls

The first construct that you’ll provide to verify mocked function use will confirm the number of times they were called. Here it is:

(defmacro verify-call-times-for [fn-name number]
  `(is (= ~number (count (@mock-calls ~(keyword fn-name))))))

This makes it easy to see if a mocked function was called a specific number of times. Another way to verify the mocked calls would be to ensure they were called with specific arguments. Because you’re recording that information as well, it’s quite easy to provide verification functions to do this:

(defmacro verify-first-call-args-for [fn-name & args]
  `(is (= '~args (first (@mock-calls ~(keyword fn-name))))))

Finally, because a mocked function may be called multiple times by the code under test, here’s a macro to verify any of those calls:

(defmacro verify-nth-call-args-for [n fn-name & args]
  `(is (= '~args (nth (@mock-calls ~(keyword fn-name)) (dec ~n)))))

Let’s look at these verification mechanisms in action:

(deftest test-filter-greater-than
  (mocking [log-call]
    (let [filtered (expenses-greater-than all-expenses 15.0)]
      (is (= (count filtered) 2))
      (is (= (:amount (first filtered)) 20.0))
      (is (= (:amount (last filtered)) 30.0)))
    (verify-call-times-for log-call 1)
    (verify-first-call-args-for log-call "expenses-greater-than" 15.0)
    (verify-nth-call-args-for 1 log-call "expenses-greater-than" 15.0)))

What you now have going is a way to mock any function so that it doesn’t get called with its regular implementation. Instead, a dummy function is called that returns nil and lets the developer also verify that the calls were made and with particular arguments. This makes testing code with various types of dependencies on external resource much easier. The syntax is also not so onerous, making the tests easy to write and read.

You can now also refactor verify-first-call-args-for in terms of verify-nth-call-args-for as follows:

(defmacro verify-first-call-args-for [fn-name & args]
  `(verify-nth-call-args-for 1 ~fn-name ~@args))

So that’s the bulk of it! Listing 10.2 shows the complete mocking and stubbing macro implementation. It allows functions to be dynamically mocked out or stubbed, depending on the requirement. It also provides a simple syntactic layer in the form of the mocking and stubbing macros, as shown previously.

Listing 10.2. Simple stubbing and mocking macro functionality for Clojure tests
(ns clj-in-act.ch10.mock-stub
  (:use clojure.test))
(def mock-calls (atom {}))
(defn stub-fn [the-function return-value]
  (swap! mock-calls assoc the-function [])
  (fn [& args]
    (swap! mock-calls update-in [the-function] conj args)
    return-value))
(defn mock-fn [the-function]
  (stub-fn the-function nil))
(defmacro verify-call-times-for [fn-name number]
  `(is (= ~number (count (@mock-calls ~(keyword fn-name))))))
(defmacro verify-nth-call-args-for [n fn-name & args]
  `(is (= '~args (nth (@mock-calls ~(keyword fn-name)) (dec ~n)))))
(defmacro verify-first-call-args-for [fn-name & args]
  `(verify-nth-call-args-for 1 ~fn-name ~@args))
(defmacro mocking [fn-names & body]
  (let [mocks (map #(list `mock-fn (keyword %)) fn-names)]
    `(with-redefs [~@(interleave fn-names mocks)]
       ~@body)))

(defmacro stubbing [stub-forms & body]
  (let [stub-pairs (partition 2 stub-forms)
        real-fns (map first stub-pairs)
        returns (map last stub-pairs)
        stub-fns (map #(list `stub-fn %1 %2) real-fns returns)]
    `(with-redefs [~@(interleave real-fns stub-fns)]
       ~@body)))

That’s not a lot of code: under 30 lines. But it’s sufficient for your purposes and indeed as a basis to add more complex functionality. We’ll now look at a couple more things before closing this section.

10.2.5. Managing stubbing and mocking state

As the tests are set up and run, you build up state for things like canned return values and metrics around what was called with what arguments. In this section, we’ll look at managing this state.

Clearing recorded calls

After a test run such as the previous one, the mock-calls atom contains all the recorded calls to mocked functions. The verification macros you create work against this to ensure that your mocks were called the way you expected. When all is said and done, though, the data that remains is useless. You can add a function to clear out the recorded calls:

(defn clear-calls []
  (reset! mock-calls {}))

In case you wondered why running the same test multiple times doesn’t cause an accumulation in the mock-calls atom, it’s because the call to stub-fn resets the entry for that function. Further, this global state will cause problems if you happen to run tests in parallel, because the recording will no longer correspond to a single piece of code under test. The atom will, instead, contain a mishmash of all calls to various mocks from all the tests. This isn’t what’s intended, so you can fix this by making the state local.

Removing global state

By removing the global mock-calls atom, you’ll be able to improve the ability of tests that use mocking to run in parallel. The first thing you’ll do is make the global binding for mock-calls dynamic:

Next, for things to continue to work as they did, you have to reestablish the binding at some point. You’ll create a new construct called defmocktest, which will be used instead of deftest. Its only job is to create a binding for mock calls before delegating back to good-old deftest:

(defmacro defmocktest [test-name & body]
  `(deftest ~test-name
     (binding [*mock-calls* (atom {})]
       (do ~@body))))

After this, your previously defined tests would need to be redefined using defmocktest:

(defmocktest test-fetch-expenses-greater-than
  (stubbing [fetch-all-expenses all-expenses]
    (let [filtered (fetch-expenses-greater-than "" "" "" 15.0)]
      (is (= (count filtered) 2))
      (is (= (:amount (first filtered)) 20.0))
      (is (= (:amount (last filtered)) 30.0)))))

And here’s the other one:

(defmocktest test-filter-greater-than
  (mocking [log-call]
    (let [filtered (expenses-greater-than all-expenses 15.0)]
      (is (= (count filtered) 2))
      (is (= (:amount (first filtered)) 20.0))
      (is (= (:amount (last filtered)) 30.0)))
    (verify-call-times-for log-call 1)
    (verify-first-call-args-for log-call "expenses-greater-than" 15.0)))

The trade-off is that you have to include the calls to your verify macros inside the scope of the call to defmocktest. This is because the mock calls are recorded inside the atom bound by the binding created by the defmocktest macro, and outside such scope there’s nothing bound to *mock-calls*.

You’ve completed what you set out to do: you started by exploring the clojure .test library and then added functionality to allow simple stubbing and mocking of functions. Our final stop will be to look at another couple of features of clojure.test.

10.3. Organizing tests

A couple other constructs that are part of the clojure.test unit-testing library are worth knowing about. They help with organizing asserts inside the body of a test function. Although it’s usually better to keep the number of asserts in each test to the lowest possible number, sometimes it’s logical to add asserts to existing tests rather than add new tests.

When a test does have several assertions, it often becomes more difficult to understand and maintain. When an assertion fails, it isn’t always clear what the specific failure is and what specific functionality is breaking. In this section, we’ll look at two macros that can help you manage assertions: the testing macro and the are macro.

The testing macro documents groups of test assertions. The are macro does two things: it removes duplication when several assertions using is are used with minor variations, and it groups such assertions together. We’ll look at an example of the testing macro in action first.

10.3.1. The testing macro

Let’s revisit the test-filter-greater-than test from the previous section. There are two distinct sets of things you’re checking for here: first that the filtering itself works and second that the call to log-call happens correctly. You’ll use the testing macro to group these according to those goals:

(defmocktest test-filter-greater-than
  (mocking [log-call]
    (let [filtered (expenses-greater-than all-expenses 15.0)]
      (testing "the filtering itself works as expected"
          (is (= (count filtered) 2))
          (is (= (:amount (first filtered)) 20.0))
          (is (= (:amount (last filtered)) 30.0))))
    (testing "Auditing via log-call works correctly"
      (verify-call-times-for log-call 1)
      (verify-first-call-args-for log-call "expenses-greater-than" 15.0))))

This code deliberately changed the number of times you expect log-call to be called to 2, so you can see how things look when this test fails:

(test-filter-greater-than)
FAIL in (test-filter-greater-than) (NO_SOURCE_FILE:1)
Auditing via log-call works correctly
expected: (clojure.core/= 2 (clojure.core/count ((clojure.core/deref clj-in-act.ch10.mock-stub2/*mock-calls*) :log-call)))
  actual: (not (clojure.core/= 2 1))

As you can see, now when anything within a group of assertions fails, the testing string is printed along with the failure. It gives immediate feedback about what the problem is, and it also makes reading and understanding the test much easier.

Now let’s look at the are macro.

10.3.2. The are macro

We’ll now look at the are macro, which is an additional construct to group assertions with and one that also helps remove unnecessary duplication. Imagine that you had to create a function to uppercase a given string:

(deftest test-to-upcase
  (is (= "RATHORE" (to-upper "rathore")))
  (is (= "1" (to-upper 1)))
  (is (= "AMIT" (to-upper "amit"))))

Here’s a function that will satisfy this test:

(defn to-upper [s]
  (.toUpperCase (str s)))

You can remove the duplication in this test by using the are macro:

(deftest test-to-upcase
  (are [l u] (= u (to-upper l))
  "RATHORE" "rathore"
  "1"       "1"
  "AMIT"    "amit"))

Using the are macro combines several forms into a single form. When any of them fail, the failure is reported as a single failure. This is why it should be used to group related assertions, not as a means to remove duplication.

10.4. Summary

In this chapter, we looked at test-driven development in Clojure. As you saw, TDD in a language such as Clojure can work as well as it does in other dynamic languages. In fact, when combined with the REPL, it gets an additional boost of productivity. The typical process is this: you write a failing unit test and follow that up with trying out various implementation ideas at the REPL. When it becomes clear what approach to take, you test various implementations quickly at the REPL. Finally, you copy the code over to the test files and add additional assertions and tests.

You then wrote some simple code to stub functions, and then you added functionality to mock functions and verify the calls made to them. Clojure made it extremely easy—and the complete code for this clocked in at fewer than 30 lines. Although it probably didn’t satisfy every requirement from a stubbing and mocking library, it served your purposes well and it can be used as the basis for something more complex. It certainly showed how easily you can implement seemingly complex things in Clojure.

Overall, this chapter demonstrated that using the REPL and test-driven development significantly amplifies the natural productivity boost that comes from using a modern and functional Lisp.

In this chapter, you created new “syntax” using macros to simplify and clarify mocking and stubbing in your testing code. In the next and final chapter, you’ll learn more advanced macro techniques and how to use them to construct your own domain-specific languages.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset