Robert “Uncle Bob” Martin has just blogged on the differences in TDD styles using Clojure, as compared to more traditional languages such as Java. Though I am a Clojure-newbie, I mostly disagree with his conclusions.
His main point is that, because Clojure is a functional language, functions have no side-effects and therefore can be used directly in the tests.
For example, the production code
(defn update-all [os]
would be tested with something like
o1 (make-object ...)
o2 (make-object ...)
os [o1 o2]
us (update-all os)
(is (= (nth us 0) (update o1)))
(is (= (nth us 1) (update o2)))
There is no reason to believe that the (update) function is side-effect-free
Changing internal values is only one way of creating side-effect. I admit that Clojure encourages coders to write code that does not change variables (if I got it right, it is definitely possible to do so, but with some additional work). However, that effect only stops at the boundaries of the language. At some point, it might access the file system or a database. Clearly, the state might change there.
Correct implementation of the (update-all) function depends on the correct implementation of (update)
Bob Martin says: ”this test simply checks that the appropriate three functions are getting called on each element of the list”.
Suppose that the (update) function does not do anything or maybe does something that does not return a value, such as printing out to the console. Then, calling it will have the same effect as not calling it at all. The test above will pass even if the (update-all) function does not provide any implementation at all. When, later, the bug is found, it will be harder to fix.
The test could be clearer (with a more powerful test framework)
One of my biggest concerns is that the test looks a lot like the code itself. Looks like duplication of information to the reader.
If there was a mock framework for Clojure, I would expect to see something like
(should-return (update 1) 1.5)
(should-return (update 3) 3.0) )
o1 (make-object 1)
o2 (make-object 3)
os [o1 o2]
us (update-all os)
(is (= (nth us 0) (1.5)
(is (= (nth us 1) (3.0)
Bob Martin is right to conclude that “Clojure without TDD is just as much a nightmare as Java or Ruby without TDD.”
But he should also make it clearer that it is lacking a mock framework (he does point to Brian Marick’s work on this).
It should be noted that it is possible to get a similar implementation style in Java as in Clojure, though it is significant work. In fact, that’s often how we use it here at Algodeal. That means mostly relying on immutable objects and state-less methods. Immutable collections from Google Collections help a lot, too. Still, we like to use mocks in our tests (too much for some, probably).
In the end, Uncle Bob’s post is another aspect of the (almost) age-old debate described by Martin Fowler: classicists vs. mockists. If you haven’t already, read Fowler’s article, it’s worth it.