How can you test ClojureScript applications and libraries?

September 19, 2015

Summary: Although it's still early, ClojureScript is rapidly maturing its testing story. There are a Leiningen plugin and a Boot task for autocompiling ClojureScript as it changes and running tests in a variety of engines.

ClojureScript comes with a built-in testing library called cljs.test. It's very similar to clojure.test which you may be familiar with. It's not exactly the same because in ClojureScript asynchronous calls rely on callbacks. Some adaptation was necessary.

I'll give you a brief introduction to setting up ClojureScript tests. It will focus on the differences between clojure.test and cljs.test. If you have never used clojure.test, check out my Intro to clojure.test course. But the code is short and clear so you should be able to follow along.

Namespace

Instead of requiring clojure.test, you'll have to get stuff from cljs.test. And since everything is a macro, you'll have to use :refer-macros. In ClojureScript, referring to macros is different from Clojure.

You'll also want to require in the namespace you're testing and refer to the functions you need. It's important to know that the :refer :all directive does not exist in ClojureScript.

(ns lab-notebook.core-test
  (:require [cljs.test :refer-macros [async deftest is testing]]
            [lab-notebook.core :refer [delete ajax-get]]))

Writing tests

First, you have to ask yourself "Is this an asynchronous call?". Do you need to wait for a callback to know if the test passed?

If you don't, that's an easier situation. You can just write your tests like in Clojure. Let's test that delete works with a few simple cases:

(deftest delete-test
  (is (= [] (delete [1] 0)))
  (is (= [2] (delete [1 2] 0)))
  (is (= [1 3] (delete [1 2 3] 1))))

If you do need to make an asynchronous call, you can use the async macro. The async block surround the code you're testing. The first argument to it is the name of a variable. It will be bound to the function to call after the test is done. Then after the test is done, you have to call that, pass or fail.

(deftest ajax-get-test
  (async done
    (ajax-get "http://www.lispcast.com/"
      (fn [response]
        (is (= 200 (:status response)))
        (done)))))

See, we bound done to a function, then called it inside the callback. Since ClojureScript, like JavaScript, has tons of callbacks, you'll be using this for sure.

Running tests

Running ClojureScript tests means you need to compile the code then send it to a JavaScript engine. The code could run differently in different engines. For instance, not all environments have console.log. Node doesn't have window, which the browsers do. So you'll have to choose which engine you want to run in.

There's a library called Doo that handles this for you. It works in Leiningen. There are other ways to run your tests, and this way of testing is still new, but I believe this is how things will commonly be tested in the future.

Here's how you can set it up.

Setting up the tester

There's a project called Karma that Doo relies on to test in browsers. You can still test in Node, PhantomJS, or other non-browser engines without it. You can install it like this (inside of your ClojureScript project directory):

> npm install karma karma-chrome-launcher karma-safari-launcher karma-cljs-test --save-dev

Leiningen

Add this plugin (check Clojars for the latest version):

:plugins [...
          [lein-doo "0.1.5-SNAPSHOT"]
          ...]

Make sure you're using org.clojure/clojurescript version 0.0-3308 or later in your dependencies. Make a new namespace in the cljs-test directory called lab-notebook.browser:

(ns lab-notebook.browser
  (:require [doo.runner :refer-macros [doo-tests]]
            [lab-notebook.core-test]))

(doo-tests 'lab-notebook.core-test)

Then set up your test build (in project.clj):

:cljsbuild {:builds
  {:browser-test {:source-paths ["cljs-src" "cljs-test"]
                  :compiler {:output-to "out/browser_tests.js"
                             :main 'lab-notebook.browser
                             :optimizations :none}}}}

Notice that the namespace name is quoted.

It's always a good idea to clean before compiling a different build:

> lein clean

Then you run:

> lein doo chrome browser-test

chrome is the engine. browser-test is the name of the build. It will autobuild browser-test and rerun the tests as the files change.

You can also test it in safari:

> lein doo safari browser-test

You can set up other builds for the different environments you want to test in.

Boot

There's a Boot task called boot-cljs-test that compiles and runs your tests. I really tried to get this working, but I think things still need to stabilize. Also, I'm not that familiar with Boot, so I may be wrong. There is an example ClojureScript application with testing here. This example does work but not with Karma.

Conclusions

Automated testing in ClojureScript is still a bit rough. The variety of JavaScript engines means more thought has to be put into what is run, where. And the pervasive asynchronous functions with callbacks make testing different from in Clojure. But testing is possible and it's improving. You can separate out the tests that can run anywhere from what needs access to the DOM or browser APIs.

If you're interested in getting started with ClojureScript, I recommend LispCast Single Page Applications with ClojureScript and Om. It uses Om to build an application from the ground up. The course teaches everything you need using animations, exercises, code screencasts, and more. It's the fastest and most effective way to learn to build Om applications. Of course we use Figwheel to compile our code in the course.

You might also like