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.exampleIf everything is okay, you should see something similar the following output.
(:use clojure.test))
(run-all-tests)
Testing clojure.walkIf 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
Testing clojure.core
(a bunch of other namespaces tested)
Testing clojure.zip
Ran 0 tests containing 0 assertions.
0 failures, 0 errors.
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-1Running the test should produce something similar to the following output.
(is (= 2 (+ 1 1))))
Testing clojure.walkWe see all of the standard clojure namespaces; however, we see our namespace (
Testing clojure.test.example
(a bunch of other namespaces tested)
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
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))If everything goes to plan, running your tests should now produce the following text towards the bottom of the output.
(deftest add-x-to-y
(is (= 5 (add 2 3))))
Ran 2 tests containing 2 assertions.At this point you might want to pass in a few different numbers to verify that add works as expected.
0 failures, 0 errors.
(deftest add-x-to-y-a-few-timesRunning the tests shows us our status
(is (= 5 (add 2 3)))
(is (= 5 (add 1 4)))
(is (= 5 (add 3 2))))
Ran 3 tests containing 5 assertions.This works perfectly fine; however,
0 failures, 0 errors.
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-areAnd, the unsurprising results.
(are [x y] (= 5 (add x y))
2 3
1 4
3 2))
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-areLeaving us with
(are [y z] (= y (:x z))
2 {:x 2}
1 {:x 1}
3 {:x 3 :y 4}))
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.
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.
Testing clojure.test.example
Ran 5 tests containing 11 assertions.
0 failures, 0 errors.
(defmethod report :begin-test-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.
(with-test-out
(when (some #(:test (meta %)) (vals (ns-interns (:ns m))))
(println "\nTesting" (ns-name (:ns m))))))
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.
(.addShutdownHookIn general, I create a test_helper.clj with the following code.
(Runtime/getRuntime)
(proxy [Thread] []
(run []
(run-all-tests))))
(ns test-helperOnce you've created a test_helper.clj you can
(: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))))
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.exampletest_helper.clj
(: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}))
(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.
Somewhat incidental, but the use of proxy in your example is unneeded. All clojure functions are Runnable so this works:
ReplyDelete(.addShutdownHook
(Runtime/getRuntime)
(Thread. run-all-tests))
I have used the following helper function to register function hooks like that before:
(defn at-exit [fn] (.addShutdownHook (Runtime/getRuntime) (Thread. fn)))
Thats a good point. Thanks Ben.
ReplyDeleteIf you don't mind enumerating all your test namespaces, you can use clojure.test/run-tests:
ReplyDeleteRuns all tests in the given namespaces; prints results.
Defaults to current namespace if none given. Returns a map
summarizing test results.
Is there any similar to the ruby autotest gem but for clojure?
ReplyDeletePedro, I'm not sure. I never used autotest in ruby, so I never bothered to look for it in clojure.
ReplyDeleteI think I remember hearing something on the mailing list stating there was a version available, but I didn't really pay attention.
Cheers, Jay
@Pedro
ReplyDeleteThere is no stand-alone library like autotest in clojure. However, circumspec (a RSpec-like library) comes with a "watch" function that behaves like autotest. I've used it on a couple of projects and it worked great.
Circumspec is being deprecated though in favor of, the unfinished, LazyTest. I have also seen people use the ruby gem "watchr" to add autotest like functionality for a variety of languages, including clojure.
being lazy is more of a reason TO use leiningen than to not use leiningen.
ReplyDeletecheers mate, nice tutorial
ReplyDeleteVery helpful! Thanks for sharing it with us.
ReplyDelete