I'll admit it, the first thing I like to do when learning a new language is fire up a
REPL. However, I'm usually ready for the next step after typing in a few numbers, strings and defining a function or two.
What feels like centuries ago, Mike Clark wrote an
article about using unit testing to learn a new language. Mike was ahead of his time. This blog entry should help you if you want to follow Mike's advice.
Luckily, Clojure has built in support for simple testing. (I'm currently using Clojure 1.2, you can download it from
clojure.org)
Before we get started, let's make sure everything is working. Save a file with the following clojure in it and run* it with clojure.
(ns clojure.test.example
(:use clojure.test))
(run-all-tests)
If everything is okay, you should see something similar the following output.
Testing clojure.walk
Testing clojure.core
(a bunch of other namespaces tested)
Testing clojure.zip
Ran 0 tests containing 0 assertions.
0 failures, 0 errors.
If you've gotten this far, you are all set to start writing your own tests. If you are having any trouble, I suggest logging into the #clojure IRC chat room on Freenode.net
The syntax for defining tests is very simple. The following test verifies that 1 + 1 = 2. You'll want to add the test after the ns definition and before the
(run-all-tests)
in the file you just created.
(deftest add-1-to-1
(is (= 2 (+ 1 1))))
Running the test should produce something similar to the following output.
Testing clojure.walk
Testing clojure.test.example
(a bunch of other namespaces tested)
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
We see all of the standard clojure namespaces; however, we see our namespace (
clojure.test.example
) in the results as well. The output at the bottom also tells us that 1 test with 1 assertion was executed.
The following example shows testing a custom add function. (we will add additional tests from here, without ever deleting the old tests)
(defn add [x y] (+ x y))
(deftest add-x-to-y
(is (= 5 (add 2 3))))
If everything goes to plan, running your tests should now produce the following text towards the bottom of the output.
Ran 2 tests containing 2 assertions.
0 failures, 0 errors.
At this point you might want to pass in a few different numbers to verify that add works as expected.
(deftest add-x-to-y-a-few-times
(is (= 5 (add 2 3)))
(is (= 5 (add 1 4)))
(is (= 5 (add 3 2))))
Running the tests shows us our status
Ran 3 tests containing 5 assertions.
0 failures, 0 errors.
This works perfectly fine; however,
clojure.test
also provides
are
for verifying several values.
The following example tests the same conditions using
are
.
(deftest add-x-to-y-a-using-are
(are [x y] (= 5 (add x y))
2 3
1 4
3 2))
And, the unsurprising results.
Ran 4 tests containing 8 assertions.
That's a simple
are
; however, you can do whatever you need in the form. Let's grab the value out of a map as an additional example.
(deftest grab-map-values-using-are
(are [y z] (= y (:x z))
2 {:x 2}
1 {:x 1}
3 {:x 3 :y 4}))
Leaving us with
Ran 5 tests containing 11 assertions.
The
is
and
are
macros will be all that you need for 90% of all the tests you'll ever want to write. For additional assertions and more details you can check out the
clojure.test documentation.
Advanced Topics (very unnecessary to get started)
I get annoyed with noise in my test results. Our results have been very noisy due to the namespace reporting. The
run-all-tests
function takes a regular expression (
documented here). We can change our test running call to include a regular expression, as the following example shows.
(run-all-tests #"clojure.test.example")
Once we switch to providing a regular expression the results should be limited to the following output.
Testing clojure.test.example
Ran 5 tests containing 11 assertions.
0 failures, 0 errors.
This approach works fine for our current sample file; however, it seems like a better solution would be to stop reporting namespaces that do not contain any tests. The following snippet changes the report
multimethod to ignore namespaces that don't contain any tests.
(defmethod report :begin-test-ns [m]
(with-test-out
(when (some #(:test (meta %)) (vals (ns-interns (:ns m))))
(println "\nTesting" (ns-name (:ns m))))))
If you're just getting started, don't worry you don't need to understand what's going on in that snippet. I've copied the original report method and made it conditional by adding the code in bold. As a result, the namespace is only printed if it contains any tests.
Now that our results are clean, let's talk about ways of getting those results.
Adding calls to the
run-all-tests
function isn't a big deal when working with one namespace; however, you'll need to get clever when you want to run a suite of tests. I've been told that
leiningen and Maven have tasks that allow you to run all the tests. You might want to start there. I don't currently use either one, and I'm lazy. I don't want to set up either, especially since all I want to do is run all my tests.
It turns out it's very easy to add a shutdown hook in Java. So, as a simple solution, I run all my tests from the Java shutdown hook.
(.addShutdownHook
(Runtime/getRuntime)
(proxy [Thread] []
(run []
(run-all-tests))))
In general, I create a test_helper.clj with the following code.
(ns test-helper
(:use clojure.test))
(defmethod report :begin-test-ns [m]
(with-test-out
(if (some #(:test (meta %)) (vals (ns-interns (:ns m))))
(println "\nTesting" (ns-name (:ns m))))))
(.addShutdownHook
(Runtime/getRuntime)
(proxy [Thread] []
(run []
(run-all-tests))))
Once you've created a test_helper.clj you can
use
test-helper (just like you
use
d clojure.test) (example below) and your tests will automatically be run on exit, and only namespaces with tests will be included in the output.
It's worth noting that some clojure.contrib namespaces seem to include tests, so in practice I end up using a regular expression that ignores all namespaces beginning with "clojure"** when running all tests. With all of those ideas combined, I find I can execute all my tests or only the tests in the current namespace very easily.
Below you can find all the code from this entry.
clojure.test.example.clj
(ns clojure.test.example
(:use clojure.test test-helper))
(deftest add-1-to-1
(is (= 2 (+ 1 1))))
(defn add [x y] (+ x y))
(deftest add-x-to-y
(is (= 5 (add 2 3))))
(deftest add-x-to-y-a-few-times
(is (= 5 (add 2 3)))
(is (= 5 (add 1 4)))
(is (= 5 (add 3 2))))
(deftest add-x-to-y-a-using-are
(are [x y] (= 5 (add x y))
2 3
1 4
3 2))
(deftest grab-map-values-using-are
(are [y z] (= y (:x z))
2 {:x 2}
1 {:x 1}
3 {:x 3 :y 4}))
test_helper.clj
(ns test-helper
(:use clojure.test))
(defmethod report :begin-test-ns [m]
(with-test-out
(if (some #(:test (meta %)) (vals (ns-interns (:ns m))))
(println "\nTesting" (ns-name (:ns m))))))
(.addShutdownHook
(Runtime/getRuntime)
(proxy [Thread] []
(run []
(run-all-tests))))
* Running a clojure file should be as easy as: java -cp /path/to/clojure.jar clojure.main -i file.to.run.clj
** (run-all-tests #"[^(clojure)].*") ; careful though, now your clojure.test.example tests will be ignored. Don't let that confuse you.