Testing Your Application with cljs.test

In this article written by David Jarvis, Rafik Naccache, and Allen Rohner, authors of the book Learning ClojureScript, we'll take a look at how to configure our ClojureScript application or library for testing. As usual, we'll start by creating a new project for us to play around with:

$ lein new figwheel testing

(For more resources related to this topic, see here.)

We'll be playing around in a test directory. Most JVM Clojure projects will have one already, but since the default Figwheel template doesn't include a test directory, let's make one first (following the same convention used with source directories, that is, instead of src/$PROJECT_NAME we'll create test/$PROJECT_NAME):

$ mkdir –p test/testing

We'll now want to make sure that Figwheel knows that it has to watch the test directory for file modifications. To do that, we will edit the the dev build in our project.clj project's :cljsbuild map so that it's :source-paths vector includes both src and test. Your new dev build configuration should look like the following:

{:id "dev"
  :source-paths ["src" "test"]

  ;; If no code is to be run, set :figwheel true for continued 
  automagical reloading
  :figwheel {:on-jsload "testing.core/on-js-reload"}

  :compiler {:main testing.core
            :asset-path "js/compiled/out"
            :output-to "resources/public/js/compiled/testing.js"
            :output-dir "resources/public/js/compiled/out"
            :source-map-timestamp true}}

Next, we'll get the old Figwheel REPL going so that we can have our ever familiar hot reloading:

$ cd testing
$ rlwrap lein figwheel

Don't forget to navigate a browser window to http://localhost:3449/ to get the browser REPL to connect.

Now, let's create a new core_test.cljs file in the test/testing directory.

By convention, most libraries and applications in Clojure and ClojureScript have test files that correspond to source files with the suffix _test. In this project, this means that test/testing/core_test.cljs is intended to contain the tests for src/testing/core.cljs.

Let's get started by just running tests on a single file. Inside core_test.cljs, let's add the following code:

(ns testing.core-test
  (:require [cljs.test :refer-macros [deftest is]]))

(deftest i-should-fail
  (is (= 1 0)))

(deftest i-should-succeed
  (is (= 1 1)))

This code first requires two of the most important cljs.test macros, and then gives us two simple examples of what a failed test and a successful test should look like.

At this point, we can run our tests from the Figwheel REPL:

cljs.user=> (require 'testing.core-test)
;; => nil

cljs.user=> (cljs.test/run-tests 'testing.core-test)

Testing testing.core-test

FAIL in (i-should-fail) (cljs/test.js?zx=icyx7aqatbda:430:14)
expected: (= 1 0)
  actual: (not (= 1 0))

Ran 2 tests containing 2 assertions.
1 failures, 0 errors.
;; => nil

At this point, what we've got is tolerable, but it's not really practical in terms of being able to test a larger application. We don't want to have to test our application in the REPL and pass in our test namespaces one by one.

The current idiomatic solution for this in ClojureScript is to write a separate test runner that is responsible for important executions and then run all of your tests. Let's take a look at what this looks like.

Let's start by creating another test namespace. Let's call this one app_test.cljs, and we'll put the following in it:

(ns testing.app-test
  (:require [cljs.test :refer-macros [deftest is]]))

(deftest another-successful-test
  (is (= 4 (count "test"))))

We will not do anything remarkable here; it's just another test namespace with a single test that should pass by itself. Let's quickly make sure that's the case at the REPL:

cljs.user=> (require 'testing.app-test)
nil
cljs.user=> (cljs.test/run-tests 'testing.app-test)

Testing testing.app-test

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
;; => nil

Perfect. Now, let's write a test runner. Let's open a new file that we'll simply call test_runner.cljs, and let's include the following:

(ns testing.test-runner
  (:require [cljs.test :refer-macros [run-tests]]
            [testing.app-test]
            [testing.core-test]))

;; This isn't strictly necessary, but is a good idea depending
;; upon your application's ultimate runtime engine.
(enable-console-print!)

(defn run-all-tests
  []
  (run-tests 'testing.app-test
             'testing.core-test))

Again, nothing surprising. We're just making a single function for us that runs all of our tests. This is handy for us at the REPL:

cljs.user=> (testing.test-runner/run-all-tests)

Testing testing.app-test

Testing testing.core-test

FAIL in (i-should-fail) (cljs/test.js?zx=icyx7aqatbda:430:14)
expected: (= 1 0)
  actual: (not (= 1 0))

Ran 3 tests containing 3 assertions.
1 failures, 0 errors.
;; => nil

Ultimately, however, we want something we can run at the command line so that we can use it in a continuous integration environment. There are a number of ways we can go about configuring this directly, but if we're clever, we can let someone else do the heavy lifting for us. Enter doo, the handy ClojureScript testing plugin for Leiningen.

Using doo for easier testing configuration

doo is a library and Leiningen plugin for running cljs.test in many different JavaScript environments. It makes it easy to test your ClojureScript regardless of whether you're writing for the browser or for the server, and it also includes file watching capabilities such as Figwheel so that you can automatically rerun tests on file changes. The doo project page can be found at https://github.com/bensu/doo.

To configure our project to use doo, first we need to add it to the list of plugins in our project.clj file. Modify the :plugins key so that it looks like the following:

:plugins [[lein-figwheel "0.5.2"]
            [lein-doo "0.1.6"]
            [lein-cljsbuild "1.1.3" :exclusions 
            [[org.clojure/clojure]]]]

Next, we will add a new cljsbuild build configuration for our test runner. Add the following build map after the dev build map on which we've been working with until now:

{:id "test"
 :source-paths ["src" "test"]
 :compiler {:main testing.test-runner
            :output-to 
            "resources/public/js/compiled/testing_test.js"
            :optimizations :none}}

This configuration tells Cljsbuild to use both our src and test directories, just like our dev profile. It adds some different configuration elements to the compiler options, however.

First, we're not using testing.core as our main namespace anymore—instead, we'll use our test runner's namespace, testing.test-runner. We will also change the output JavaScript file to a different location from our compiled application code. Lastly, we will make sure that we pass in :optimizations :none so that the compiler runs quickly and doesn't have to do any magic to look things up.

Note that our currently running Figwheel process won't know about the fact that we've added lein-doo to our list of plugins or that we've added a new build configuration. If you want to make Figwheel aware of doo in a way that'll allow them to play nicely together, you should also add doo as a dependency to your project. Once you've done that, exit the Figwheel process and restart it after you've saved the changes to project.clj.

Lastly, we need to modify our test runner namespace so that it's compatible with doo. To do this, open test_runner.cljs and change it to the following:

(ns testing.test-runner
  (:require [doo.runner :refer-macros [doo-tests]]
            [testing.app-test]
            [testing.core-test]))

;; This isn't strictly necessary, but is a good idea depending
;; upon your application's ultimate runtime engine.
(enable-console-print!)

(doo-tests 'testing.app-test
           'testing.core-test)

This shouldn't look too different from our original test runner—we're just importing from doo.runner rather than cljs.test and using doo-tests instead of a custom runner function. The doo-tests runner works very similarly to cljs.test/run-tests, but it places hooks around the tests to know when to start them and finish them. We're also putting this at the top-level of our namespace rather than wrapping it in a particular function.

The last thing we're going to need to do is to install a JavaScript runtime that we can use to execute our tests with. Up until now, we've been using the browser via Figwheel, but ideally, we want to be able to run our tests in a headless environment as well. For this purpose. we recommend installing PhantomJS (though other execution environments are also fine).

If you're on OS X and have Homebrew installed (http://www.brew.sh), installing PhantomJS is as simple as typing brew install phantomjs. If you're not on OS X or don't have Homebrew, you can find instructions on how to install PhantomJS on the project's website at http://phantomjs.org/. The key thing is that the following should work:

$ phantomjs -v
2.0.0

Once you've got PhantomJS installed, you can now invoke your test runner from the command line with the following:

$ lein doo phantom test once

;; ======================================================================
;; Testing with Phantom:


Testing testing.app-test

Testing testing.core-test

FAIL in (i-should-fail) (:)
expected: (= 1 0)
  actual: (not (= 1 0))

Ran 3 tests containing 3 assertions.
1 failures, 0 errors.
Subprocess failed

Let's break down this command. The first part, lein doo, just tells Leiningen to invoke the doo plugin. Next, we have phantom, which tells doo to use PhantomJS as its running environment.

The doo plugin supports a number of other environments, including Chrome, Firefox, Internet Explorer, Safari, Opera, SlimerJS, NodeJS, Rhino, and Nashorn.

Be aware that if you're interested in running doo on one of these other environments, you may have to configure and install additional software. For instance, if you want to run tests on Chrome, you'll need to install Karma as well as the appropriate Karma npm modules to enable Chrome interaction.

Next we have test, which refers to the cljsbuild build ID we set up earlier. Lastly, we have once, which tells doo to just run tests and not to set up a filesystem watcher. If, instead, we wanted doo to watch the filesystem and rerun tests on any changes, we would just use lein doo phantom test.

Testing fixtures

The cljs.test project has support for adding fixtures to your tests that can run before and after your tests. Test fixtures are useful for establishing isolated states between tests—for instance, you can use tests to set up a specific database state before each test and to tear it down afterward. You can add them to your ClojureScript tests by declaring them with the use-fixtures macro within the testing namespace you want fixtures applied to.

Let's see what this looks like in practice by changing one of our existing tests and adding some fixtures to it. Modify app-test.cljs to the following:

(ns testing.app-test
  (:require [cljs.test :refer-macros [deftest is use-fixtures]]))

;; Run these fixtures for each test.

;; We could also use :once instead of :each in order to run
;; fixtures once for the entire namespace instead of once for
;; each individual test.
(use-fixtures
  :each
  {:before (fn [] (println "Setting up tests..."))
   :after (fn [] (println "Tearing down tests..."))})

(deftest another-successful-test
  ;; Give us an idea of when this test actually executes.
  (println "Running a test...")
  (is (= 4 (count "test"))))

Here, we've added a call to use-fixtures that prints to the console before and after running the test, and we've added a println call to the test itself so that we know when it executes. Now when we run this test, we get the following:

$ lein doo phantom test once

;; ======================================================================
;; Testing with Phantom:


Testing testing.app-test
Setting up tests...
Running a test...
Tearing down tests...

Testing testing.core-test

FAIL in (i-should-fail) (:)
expected: (= 1 0)
  actual: (not (= 1 0))

Ran 3 tests containing 3 assertions.
1 failures, 0 errors.
Subprocess failed

Note that our fixtures get called in the order we expect them to.

Asynchronous testing

Due to the fact that client-side code is frequently asynchronous and JavaScript is single threaded, we need to have a way to support asynchronous tests. To do this, we can use the async macro from cljs.test. Let's take a look at an example using an asynchronous HTTP GET request.

First, let's modify our project.clj file to add cljs-ajax to our dependencies. Our dependencies project key should now look something like this:

:dependencies [[org.clojure/clojure "1.8.0"]
               [org.clojure/clojurescript "1.7.228"]
               [cljs-ajax "0.5.4"]
               [org.clojure/core.async "0.2.374"
                :exclusions [org.clojure/tools.reader]]]

Next, let's create a new async_test.cljs file in our test.testing directory. Inside it, we will add the following code:

(ns testing.async-test
  (:require [ajax.core :refer [GET]]
            [cljs.test :refer-macros [deftest is async]]))

(deftest test-async
  (GET "http://www.google.com"
       ;; will always fail from PhantomJS because
       ;; `Access-Control-Allow-Origin` won't allow
       ;; our headless browser to make requests to Google.
       {:error-handler
        (fn [res]
          (is (= (:status-text res) "Request failed."))
          (println "Test finished!"))}))

Note that we're not using async in our test at the moment.

Let's try running this test with doo (don't forget that you have to add testing.async-test to test_runner.cljs!):

$ lein doo phantom test once

...

Testing testing.async-test

...

Ran 4 tests containing 3 assertions.
1 failures, 0 errors.
Subprocess failed

Now, our test here passes, but note that the println async code never fires, and our additional assertion doesn't get called (looking back at our previous examples, since we've added a new is assertion we should expect to see four assertions in the final summary)! If we actually want our test to appropriately validate the error-handler callback within the context of the test, we need to wrap it in an async block. Doing so gives us a test that looks like the following:

(deftest test-async
  (async done
    (GET "http://www.google.com"
         ;; will always fail from PhantomJS because
         ;; `Access-Control-Allow-Origin` won't allow
         ;; our headless browser to make requests to Google.
         {:error-handler
          (fn [res]
            (is (= (:status-text res) "Request failed."))
            (println "Test finished!")
            (done))})))

Now, let's try to run our tests again:

$ lein doo phantom test once

...

Testing testing.async-test
Test finished!

...

Ran 4 tests containing 4 assertions.
1 failures, 0 errors.
Subprocess failed

Awesome! Note that this time we see the printed statement from our callback, and we can see that cljs.test properly ran all four of our assertions.

Asynchronous fixtures

One final "gotcha" on testing—the fixtures we talked about earlier in this article do not handle asynchronous code automatically. This means that if you have a :before fixture that executes asynchronous logic, your test can begin running before your fixture has completed! In order to get around this, all you need to do is to wrap your :before fixture in an async block, just like with asynchronous tests. Consider the following for instance:

(use-fixtures :once
  {:before
   #(async done
      ...
      (done))
    :after
    #(do ...)})

Summary

This concludes our section on cljs.test. Testing, whether in ClojureScript or any other language, is a critical software engineering best practice to ensure that your application behaves the way you expect it to and to protect you and your fellow developers from accidentally introducing bugs to your application. With cljs.test and doo, you have the power and flexibility to test your ClojureScript application with multiple browsers and JavaScript environments and to integrate your tests into a larger continuous testing framework.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Learning ClojureScript

Explore Title