How can you test ClojureScript applications and libraries?
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.
UPDATE (26 August 2017): I just tested the instructions here and they still work. I've made a repository with the code in this article so you can see it working.
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 "/"
(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.