TDD in Clojure: a sketch (part 1)
I continue to use little experiments to help me think through TDD in Clojure. (I plan to begin a realistic experiment soon.) Right now, I’m mainly focused on three questions:
-
What would mocking or stubbing mean in a strict(ish) functional language?
-
What’d be a good mocking notation for Clojure?
-
How do you balance the outside-in style associated with mocks and the bottom-up style that the REPL (interpreter) encourages?
Here’s an example from Conway’s Game of Life. It begins with an implementation suggestion from Paul Blair and Michael Nicholaides at the Philly Code Retreat. Instead of thinking of the board as a 2×2 array of cells, with some of them dead and some alive, think instead only of living cells, each of which knows its coordinates. Here’s an example that shows how “blinkers” blink from generation to generation.
A couple of things have happened here:
-
This is my notation for a straightforward non-stubbing test. The value on the left is executed and it’s compared (for equality) to the value on the right.
-
I’ve started coding outside-in, and I’ve named the first function I need:
next-world
.
The Blair/Nicholaides approach advances the “world” to the next generation by (conceptually) adding dead cells around the edge of all the living cells, running the normal life rules that govern how cells change because of their neighbors, and then throwing away all the cells that end up dead. In other words:
-
The
pending
bit is just there because (sadly) Clojure makes you declare functions before mentioning them.pending
just creates functions that print that they’ve not yet been implemented. -
The rest of the code flows the
world
argument through a pipeline of three functions. If you’re not familiar with the->
macro, the result is the same as this:I don’t feel the need to test this code now because it’s really declarative—it says what it means to produce a next world under this approach. (It will be tested in the very end by the “integration test” that shows a blinker working.)
I can now implement any of the three new functions. I’ll pick tick
because it seems to be the heart of the matter. Here’s a first implementation:
There are two odd things going on here.
-
I think of the Clojure programs I’ve written this way: there’s a big stateful universe out there. At some moment, a snapshot of relevant bits of that universe is presented to my pure functional program. It churns away, presents the results back to the big stateful universe, and then disappears. Given that bias, I find it convenient to think of the universe-bits not as data to be passed around, but as the unchanging background behind my code’s calculations. Any of that code should be able to reach out and grab bits of the relevant background by calling dynamically bound functions of no arguments.
Because of
against-background
, any code operating insidesuccessor
(or inside functions it calls) can call(world)
and magically get the originalbordered-world
. As you’ll see, this lets me put off committing to a representation for cells. (The representation I used for the blinkers isn’t enough after the border’s been added, since there’s now a need to distinguish between living and dead cells.) -
This essay is supposed to be a sketch of TDD in Clojure, but here’s the second function I’ve written without tests. What’s up with that? I’ve tentatively concluded that testing functions that map one function over a sequence is the moral equivalent of testing getters and setters: not worth the trouble. But don’t fear,
successor
is worth testing, and we’ll do that next. Almost next.
First, stubbing function calls.
In object-oriented languages, I think of mock-driven-design as a way of teasing out collaborators for the object I’m building. I push responsibilities for work onto objects that I’ll implement later. Mocking lets me defer the implementation of those objects until I’m ready, and creating some examples of the API teaches me the (implicit) specification for the new object.
I’ve found that with pure functional programs that don’t modify state, it makes more sense to think of a function like (f 2) => 4
as a fact. What I’m doing as I test-drive a function is describing how facts about its inputs and outputs depend on other facts, in an almost Prolog-like way. For example, consider this code:
That says that, for any cell you care to provide, f
of that cell will be 10, provided g
of that cell is true and h
is 2. If either of those latter two facts don’t apply to the cell, I’m not saying what f
’s value is.
I use the funny ...cell...
notation in the way that mathematicians use n to talk about any integer. (They call that universal quantification.) I don’t want to create a particular cell because I might need to specify properties that have nothing to do with the function I’m working on. This notation says that nothing about the cell is relevant except for what comes after the provided
.
Here’s one way to write a Life rule in this notation:
The falsey
bit in the first line is because Clojure has two distinct values that can mean “false”. falsey
is a function that takes the result of the left-hand side and fails the test if that result is anything other than one of the two false values. I’m using it because I don’t want to overspecify living?
. There’s no reason to care which of the two “false” values it returns.
There’s a problem with this test, though. Remember what I said above: the left-hand side gets evaluated and handed to falsey
. That means living?
has to have a definition—which means I’d have to settle on how the code knows whether a cell is alive or dead. I like doing one thing at a time and putting off decisions as long as I can, and right now I’d rather be focused on successor
instead of cell representations.
Here’s a way to defer that decision:
Here I’m saying something subtly different than before. I’m saying that the result of successor
is specifically that cell produced by calling killed
on the original cell. The =means=>
notation tells the framework to create a mock instead of evaluating the right-hand side for its value. In a more familiar mocking syntax (for Ruby), the whole test is equivalent to:
OK. The next figure gives the whole set of Life rules, expressed as executable tests. (Well, executable as soon as I implement the testing framework.) Notice that I called the outer wrapper know
(a fact) instead of example
. know
seems more appropriate for rules. The two forms mean the same thing.
Notice also that I implemented a notation for saying “run this test for each value in a sequence”. The use of commas, as in [4,,,8]
, indicates that—conceptually—the fact is true for all values four through eight. Only the ones listed are actually tried. (Commas count as >white space in Clojure.)
This isn’t the tersest possible format—a table would be better—but it’ll do. I think it’s reasonably readable. Do you?
Here, for reference, is code that passes the test:
We now have an expanded choice of functions to write:
I could go breadth-first—with border
and unborder
—or go depth-first with one of the functions on the second line. In this particular case, I’d rather go depth first. I’ve avoided deciding on a representation, so I don’t know yet what border
should do.
If this installment meets your approval, I’ll add another one that begins work on—oh—probably living-neighbor-count
is the most complicated, so it’s a good one to chip away at.
June 13th, 2010 at 7:31 pm
I’ve noticed you haven’t been getting many comments. I know how discouraging that can be.
Please continue. I’m finding this very interesting, and it is making me interested in trying Clojure myself.
June 14th, 2010 at 1:48 am
[…] in Clojure: a sketch (part 1. My workflow and notation. (here, via @marick) — The author is trying to find what would be a realistic Test Driven […]
June 16th, 2010 at 11:06 am
[…] Part 1 […]
June 17th, 2010 at 3:02 pm
[…] Part 1 Part 2 […]
June 30th, 2010 at 6:05 pm
[…] devoted reader will recall that I earlier sketched a “little language” for mocking Clojure functions. I’ve finished implementing a […]
October 10th, 2010 at 5:16 pm
[…] be able to stretch by occasionally going slower while I experiment with techniques. (My work on outside-in TDD in Clojure is an example.) I’m willing to be paid less in order to improve […]