Wednesday, August 11, 2010

clojure.test Introduction

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 used 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.

9 comments:

  1. Somewhat incidental, but the use of proxy in your example is unneeded. All clojure functions are Runnable so this works:

    (.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)))

    ReplyDelete
  2. Anonymous4:35 PM

    Thats a good point. Thanks Ben.

    ReplyDelete
  3. If you don't mind enumerating all your test namespaces, you can use clojure.test/run-tests:

    Runs all tests in the given namespaces; prints results.
    Defaults to current namespace if none given. Returns a map
    summarizing test results.

    ReplyDelete
  4. Is there any similar to the ruby autotest gem but for clojure?

    ReplyDelete
  5. Anonymous8:00 AM

    Pedro, I'm not sure. I never used autotest in ruby, so I never bothered to look for it in clojure.

    I think I remember hearing something on the mailing list stating there was a version available, but I didn't really pay attention.

    Cheers, Jay

    ReplyDelete
  6. @Pedro

    There 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.

    ReplyDelete
  7. Anonymous12:04 AM

    being lazy is more of a reason TO use leiningen than to not use leiningen.

    ReplyDelete
  8. Anonymous5:26 PM

    cheers mate, nice tutorial

    ReplyDelete
  9. Very helpful! Thanks for sharing it with us.

    ReplyDelete

Note: Only a member of this blog may post a comment.