Chapter 8. Test-driven development and more

 

This chapter covers

  • 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 low 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 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 (you visited the REPL in chapter 2, and in this chapter, you’ll see a lot more of it). As you’ll discover, this combination of TDD and the REPL makes for a productive development environment. The specific unit-testing framework you’ll explore is called test-is, and it comes as a standard part of the Clojure distribution. Finally, you’ll look into mocking and stubbing needs that you might run into, and you’ll write code to handle such situations.

8.1. Getting started with TDD

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. The example in the next section will be a little more involved and will deal with a situation that demands mocking and stubbing of functions. Let’s get started.

8.1.1. Example: dates and string

In test-driven development, 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. 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 internals.

The 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 chapter08 within your source directory):

(ns chapter08.date-operations-spec
  (:use chapter08.date-operations)
  (:use clojure.test))

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

You’re using the test-is unit-testing framework for Clojure, which began life as an external library and later was included as part of the distribution. 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 test-is 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 in the following paragraphs.

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

Could not locate chapter08/date_operations__init.class or
  chapter08/date_operations.clj on classpath:
  [Thrown class java.io.FileNotFoundException]

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:

Unable to resolve symbol: date in this context
  [Thrown class java.lang.Exception]

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 chapter08.date-operations)

(defn date [date-string])

(defn day-from [d])

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

user> (use 'clojure.test)
nil
user> (run-tests 'chapter08.date-operations-spec)

Testing chapter08.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 chapter08.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 SimpleDate-Format classes. You can use these to convert strings into dates. You can experiment with them on the REPL:

user> (import '(java.text SimpleDateFormat))
java.text.SimpleDateFormat

user> (def f (SimpleDateFormat. "yyyy-MM-dd"))
#'user/f

user> (.parse f "2010-08-15")
#<Date Sun Aug 15 00:00:00 PDT 2010>

So you know SimpleDateFormat will work, and now you can check out the Gregorian-Calendar:

user> (import '(java.util GregorianCalendar))
java.util.GregorianCalendar

user> (def gc (GregorianCalendar. ))
#'user/gc

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

user> (def d (.parse f "2010-08-15"))
#'user/d

user> (.setTime gc d)
nil

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

(ns chapter08.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))))

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

user> (import '(java.util Calendar))
java.util.Calendar

user> (.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 tests should pass now. Remember that in order 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:

user> (run-tests 'chapter08.date-operations-spec)

Testing chapter08.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 tests.

Month-From, Year-From

The tests for getting the month and year are 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 tests should pass:

user> (run-tests 'chapter08.date-operations-spec)

Testing chapter08.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. Let’s add an as-string function that can convert your date objects into the string format.

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.contrib.str-utils in the namespace via a use clause:

(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:

user> (def d (date "2010-12-25"))
#'user/d

user> (as-string d)
"2010-12-25"

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

user> (run-tests 'chapter08.date-operations-spec)

Testing chapter08.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 in order to get your test to pass. 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 tests now should show a better response:

user> (run-tests 'chapter08.date-operations-spec)

Testing chapter08.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.

Incrementing, Decrementing

Because you’re just getting started and don’t want to lose momentum, we’ll postpone refactoring until after adding one more feature. You’ll add functionality to advance and turn back dates. You’ll start with addition, and as usual, you’ll write a test:

(deftest test-incrementing
  (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:

user> (def d (date "2009-10-31"))
#'user/d

user> (.add d Calendar/DAY_OF_MONTH 1)
nil

user> (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:

user> (run-tests 'chapter08.date-operations-spec)

Testing chapter08.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}

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!

In order 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, the tests all pass, allowing us to tackle decrementing. Again, start with the 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)))

This passes 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, and we’re now going to improve it.

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; in order 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). Our tests from the previous section will serve this purpose.

Let’s begin our 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 reduced more 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.

You could imagine a similar refactoring being applied to the month-from, day-from, and year-from functions, but the decision to do that and the implementation are left as an exercise for you.

This section showed the usage of the built-in Clojure unit-testing library called test-is. As you saw through the course of building our 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 a REPL, the combination can result in an ultra-fast development cycle while keeping quality high. In our next section, you’ll see how you can write a simple mocking and stubbing framework to make your unit testing even more effective.

8.2. Mocking and stubbing things

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.

8.2.1. Example: expense finders

In this example, you’ll write a few functions to load expense objects from a data store and to filter them based on some criteria (such as greater than a particular amount). Because you’re dealing with money, you’ll also throw in a requirement that your functions must log to an audit log. This example is a bit contrived, but it will serve our purposes quite well.

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 is the code you’re trying to test.

Listing 8.1. Example code that fetches and filters expenses from a data store
(ns chapter08.expense-finders
  (:use clojure.contrib.str-utils))

(defstruct expense :amount :date)
(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 structs
)

(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, the expense struct map is used to represent expense objects. 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. In order 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, you’ve tested the persistence of data to and from the data store elsewhere in your code, 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.

8.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 [(struct-map expense :amount 10.0 :date "2010-02-28")
                   (struct-map expense :amount 20.0 :date "2010-02-25")
                   (struct-map expense :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 that it returns all-expenses? In order 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. This makes it almost trivial, and here’s the implementation:

(ns chapter08.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)]
    `(binding [~@(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. Admittedly, it doesn’t do everything a fuller-featured stubbing framework might, but it gets the job done. Before you look at a sample expansion of this macro, let’s look at an example:

(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:

user> (some-client)
6 4/3
nil

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

user> (stubbing [calc-x 1
                 calc-y 2]
        (some-client))
1 2

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

user> (macroexpand-1' (stubbing [calc-x 1 calc-y 2]
        (some-client)))
  (clojure.core/binding [calc-x (constantly 1)
                         calc-y (constantly 2)]
    (some-client))

The constantly function does the job well, but in order 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 is equivalent to constantly. The rewritten code is shown here:

(defn stub-fn [return-value]
  (fn [& args]
    return-value))

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

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

8.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 chapter08.expense-finders-spec
  (:use chapter08.expense-finders
        clojure.test))
(deftest test-filter-greater-than
  (let [fetched [(struct-map expense :amount 10.0 :date "2010-02-28")
                 (struct-map expense :amount 20.0 :date "2010-02-25")
                 (struct-map expense :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:

user> (run-tests 'chapter08.expense-finders-spec)

Testing chapter08.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 our 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 framework.

8.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.

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 in order 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)]
    `(binding [~@(interleave real-fns stub-fns)]
       ~@body)))

OK, 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)]
    `(binding [~@(interleave fn-names mocks)]
       ~@body)))

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

(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)))))

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 usage 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))))))

Let’s look at these two 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.

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)))))

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 8.2 shows the complete mocking and stubbing 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 8.2. Simple stubbing and mocking functionality for Clojure tests
(ns chapter08.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-first-call-args-for [fn-name & args]
  `(verify-nth-call-args-for 1 ~fn-name ~@args))

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

(defmacro mocking [fn-names & body]
  (let [mocks (map #(list 'mock-fn (keyword %)) fn-names)]
    `(binding [~@(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)]
    `(binding [~@(interleave real-fns stub-fns)]
        ~@body)))

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

Clearing recorded calls

After a test run such as the previous one, our 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. Let’s add a function to clear out the recorded calls:

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

On a separate note, 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 to get rid of the global binding for mock-calls:

(def mock-calls)

Next, in order 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 defmock-test:

(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 necessarily 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 test-is framework 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 test-is.

8.3. Organizing tests

A couple of other constructs that are part of the test-is unit-testing framework 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 adding 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. The testing macro comes in handy by documenting groups of asserts.

Finally, 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.

8.3.1. Testing

Let’s revisit our test-filter-greater-than test from the previous section. There are two distinct sets of things you’re checking for here: the fact that that filtering itself works and 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 2)
      (verify-first-call-args-for log-call "expenses-greater-than" 15.0))))

We’ve deliberately changed the number of times log-call is expected to be called to 2, so you can see how things look when this test fails:

user> (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
     chapter08.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 also makes reading and understanding the test much easier.

Now let’s look at the are macro.

8.3.2. are

We’ll now look at an additional construct to group assertions with, one that also helps remove unnecessary duplication. Imagine that you had to create a function to upper case 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 assertion. When any of them fail, the failure is reported as a single assertion failure. This is one reason why it should be used for related assertions, not as a means to remove duplication.

8.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. You can write a failing unit test and try out various implementation ideas at the REPL. When it’s clear what approach to take, it’s easy to write the code to pass the test. Similarly, it’s easy to test implementations quickly at the REPL and then to copy the code over to the test files to add additional assertions and tests.

You then wrote a simple framework 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 thirty lines of code. Although it probably didn’t satisfy every requirement from a stubbing and mocking library, it served our purposes well and it can be used as the basis for something more complex. It certainly showed how easily, seemingly complex things can be implemented in Clojure.

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

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

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