Top Down TDD in Clojure with Brian Marick
This week I attended a two-day course on top-down TDD in Clojure, led by Brian Marick. The course was organised through the London agile community group the Extreme Tuesday Club (XTC) and generously supported by Zuhlke Engineering who provided the venue, allowing us the use one of their conference rooms, and access to enough tea and coffee to get us through two days of thinking and programming.
The aim of the course was to learn about, and to try out, techniques for test-driven development in Clojure, especially using Midje - a tool that Brian wrote to enable programmers to use a form of mocking during development of Clojure programs. Using Midje allows programmers to explore a problem working in a breadth-first manner, without having to define all of the supporting functions at lower levels of implementation until later on.
The course attracted a pretty experienced group of attendees, with some regular Clojure programmers, including a couple of people who help to organise the London Clojure Dojo, and some TDD experts including Steve Freeman and Nat Pryce, the authors of GOOS. This led to some very interesting and detailed discussions, and also meant that the course quickly changed from being an instructor-led tutorial into more of an exploratory workshop, debating the pros and cons of various approaches to problems.
A Midje test defines some facts about the world as we would like it to be, once we have finished our program. For example we could be writing a program to calculate the vegetarian breakfast menu for a hotel. We can state a fact that we want to be true, and some clauses to set up the situation under which this could be the case. For example, we can say that the vegetarian menu will be cereal, provided that the full menu is cereal or a full-english, a full-english contains meat, but cereal does not. Using Midje we could define that like this:
(ns midje-example.core (:use midje.sweet))
(fact (veggie-breakfast) => [..cereal..]
(provided (breakfast-menu) => [..cereal.. ..full-english..])
(provided (contains-meat? ..full-english..) => true)
(provided (contains-meat? ..cereal..) => false)
)
We can use Midje’s meta-constants (denoted by two dots before and after the name, e.g.. ..cereal..
) to fill in the test, even though we have not yet decided the data structure that we will use to model food items or menus. We use the provided
clauses to define sufficient properties of the meta-constants to make the test work. This is the top-down nature of this style of TDD, we will move on to making these decisions later, but can get our higher level function working first.
Clojure isn’t happy if we use things that are not defined, so we create a skeleton definition of veggie-breakfast
which is our function under test, and use Midje to create skeletons for our other unfinished functions.
(ns midje-example.core (:use midje.sweet))
(unfinished breakfast-menu contains-meat?)
(defn veggie-breakfast [] [])
(fact (veggie-breakfast) => [..cereal..]
(provided (breakfast-menu) => [..cereal.. ..full-english..])
(provided (contains-meat? ..full-english..) => true)
(provided (contains-meat? ..cereal..) => false)
)
Then we can run Midje, and it tells us that our fact is not confirmed - which is not unexpected, as we haven’t written our function yet. Now we complete the implementation, run it again, and show that things are working.
(ns midje-example.core (:use midje.sweet))
(unfinished breakfast-menu contains-meat?)
(defn veggie-breakfast []
(filter (complement contains-meat?) (breakfast-menu))
)
(fact (veggie-breakfast) => [..cereal..]
(provided (breakfast-menu) => [..cereal.. ..full-english..])
(provided (contains-meat? ..full-english..) => true)
(provided (contains-meat? ..cereal..) => false)
)
At the moment, for the smoothest experience, you need to use the emacs integration that Brian has developed. There is a leiningen plugin, but it is quite slow to run, so running even a small test suite can take a good few seconds, which is slightly annoying. It would be nice to have some effective tooling to work with the La Clojure plugin for IntelliJ. I think that that would help with Midje adoption.
We spent a good deal of time discussing static versus dynamic dependencies in Clojure programs, how to create seams to inject dependencies on underlying components, and whether doing so was required or idiomatic in Clojure. I don’t think we reached any firm conclusions, but it was interesting to explore possibilities and constraints.