ClojureScript Test Setup

To begin testing a front or backend application you first need to setup your Test Toolchain. A Test Toolchain are all the tools that allow you to write, find, run and report on the tests you write in any software project. This post will walk through how to setup a Test Toolchain from scratch for a ClojureScript app.

We will start by showing you the most basic tools you can use (they are still powerful!), what they do and how to configure them.

Create a Project #

Start by creating a basic ClojureScript app. The easiest way to do this is by using create reagent app which automatically builds a modern ClojureScript app for us. Here is how you go about doing that:

  • Move to a directory where you want your project to live
  • Run the following command in your terminal
clj -Sdeps '{:deps
              {seancorfield/clj-new {:mvn/version "1.1.321"}}}' \
  -X clj-new/create \
  :template '"https://github.com/tkjone/create-reagent-app@7500dd43dc1be88a762ec2d74aad1f2c2c29842d"' \
  :name nike/nike-app

Once the above command is done you will have a project called nike-app. It should look like this:

nike-app
├── README.md
├── deps.edn
├── dev.cljs.edn
├── prod.cljs.edn
├── resources
│   └── public
│       ├── index.html
│       └── style.css
├── src
│   └── nike
│       └── nike_app.cljs
└── test
    └── nike
        └── nike_app_test.cljs

Now, move into the project you just created

cd nike-app

Now let's make sure everything is working as expected by running the following command:

clj -M:dev

If everything worked, a new browser window should automatically open to http://localhost:9500/ and you should see the following:

screenshot of example hello clojurescript site

I now want to draw your attention to the folder structure and naming conventions of the project we just built:

├── src
│   └── nike
│       └── nike_app.cljs
└── test
    └── nike
        └── nike_app_test.cljs # <--- notice the `_test`

It's an accepted practice in Clojure to have your tests mirror your src directory structure. Then, when you name the actual files containing your tests they get suffixed with _test.

Write a Test #

Now that we have a project to work in, let's write our first test!

Open nike_app.cljs. You will notice that some code already exists in this namespace. Delete that code below the namespace decleration at the top so the whole file looks like this:

(ns nike.nike-app)

(defn add
  [a b]
  (+ a b))

Now we can write our first ClojureScript test! I will show you what it looks like first and then we will explain it after.

Open nike_app_test.cljs and write the following exactly as you see:

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

(deftest test-add-function
  (is (= (nike-app/add 1 1) 2)))

Okay, let's review the test we just wrote. We will start with explaining what what cljs.test is. cljs.test is a minimal library with everything you need to test ClojureScript code. Among other things, it comes with:

  • Test Decleration Utils e.g. deftest
    • deftest defines a test where test-add-function is a name of our test
  • Test Assertion Library e.g. is
    • is function is a convenience wrapper around a try-catch block
  • Test Runner
    • we haven't introduced this to you yet, but it's called run-tests

Now that we have written a test, lets go over how to run the test. Now, there are a few way you can run the tests so we will go over some of the main options.

Run tests with cljs-main #

To run our tests, we need to create a namespace that will import all of our tests and then run them. We can refer to this namespace as our test_runner, but you can also think of it as the test entrypoint.

Start by creating a new namespace in test/nike called test_runner.cljs. Open that file and make it look like this:

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

(run-tests 'nike.nike-app-test)

What the above does is:

  • Import our test namespaces e.g. nike.nike-app-test
  • Run all of our rests by calling run-tests

From here, we have to build our tests and then run them in the browser. We can do this by adding the following alias to our deps.edn file.

{:test {:main-opts ["-m"  "cljs.main"
                    "-c"  "nike.test_runner"
                    "-r"]}}

The big beautiful takeaway here is that there is nothing magical happening. This is just cljs code compiled like any other cljs file and than added to an index.html file. The index.html file can be customized if you like.

From here, we just have to run the tests. To do this, go to your terminal and run the test alias:

clj -M:test

What the above alias is going to do is:

  • compile your cljs to js
  • automatically open your app in your default browser
  • render the default ClojureScript index.html

But you will notice that you won't see the tests anywhere. So, what happened? Your test report or output is actually not display in the browser, but in your terminal and will look something like this:

screenshot of example hello clojurescript site

Note that you could also have the test report displayed in your browser console by adding (enable-console-print!) in your test-runner namespace before you invoke run-tests:

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

(enable-console-print!) ; new

(run-tests 'nike.nike-app-test)

The last note I want to make is that if you are writing tests like this, you likely don't want to have to manually re-build your tests every time you make a change to your src or test code. To improve this workflow you can tell ClojureScript to watch the src and test directory so that every time you update files in your src or test dir they will automatically be recompiled on save. To make this happen, update your :test alias to look like this:

{:test {:main-opts ["-m"  "cljs.main"
                    "-w"  "src:test" ; new
                    "-c"  "nike.test_runner"
                    "-r"]}}

And that's everything involved in setting up a ClojureScript only Test Toolchain. For those who want to take it a step further, let's see how we can configure figwheel to run these tests.

Run tests with figwheel #

Because we are using the Create Reagent App template, we have everything we need to run our tests using `figwheel. In fact, most everything we needed from the previous section will be used in this section with a few changes. As you will see, one awesome feature of using figwheel for testing is you get more tooling, with less configuration.

To recap, we already have:

  • A test
  • A test runner

The only thing we have to change is a small piece of our test runner. Open up test_runner.cljs and update the following

(ns nike.test-runner
  (:require
    [cljs-test-display.core] ; new
    [figwheel.main.testing :refer-macros [run-tests]] ; new
    [nike.nike-app-test]))

(run-tests (cljs-test-display.core/init! "app-testing")) ; updated

The next step is to open your dev.cljs.edn file and add a :extra-main-files key set like this:

^{:watch-dirs       ["src" "test"] ; updated
  :css-dirs         ["resources"]
  :extra-main-files {:testing {:main nike.test-runner}}}; new
{:main nike.nike-app}

What the above does is build and run your tests, like we did with the :test alias in the previous section. The difference is that we are going to do this as part of our :dev alias. This is amazing because it means we have a file watcher, HMR and this is running in the same process as our development build. So now, we can develop our app and tests at the same time in one simple command.

To run the above:

clj -M:dev

and now you can visit your app at http://localhost:9500 and your tests at http://localhost:9500/figwheel-extra-main/testing

and when you visit localhost:9500/figwheel-extra-main/testing you should see something like this:

screenshot of figwheel test runner

The above is a graphical version of the textual test report that we got when we ran the tests using cljs.test. From here, you can modify your code and your figwheel-extra-main/testing will not only rebuild, but live update creating a nice developer experience.

Runtime Environments #

Up until now, we haven't really discussed "Runtime Environments". Yet, I feel this is something important to discuss because its something that confusing in JavaScript land and sooner or later, you have to consider it in ClojureScript as well.

Consider this, when you run code in your ClojureScript REPL, where is that code run? Is it the browser? node? The answer is that it depends on how you run your REPL. If you run it like we did in the above run tests with cljs.main section your code will be run in a Browser Environment. If you were to set --repl-env to node then your code would be run in a Node Environment. This is what we mean by understanding which environment your code is running in.

The importance of the environment is your code, when run in production, is targeting one of these and this means that the JS API's you have available to you will be different for each and the performance of your tests. For example, you won't have the window object in Node, so it wouldn't be a good idea to test your browser code in a Node environment.

The second point is test performance. As it turns out, it's not always desirable or possible to test your browser code directly in the browser. For example, you may run into a scenario where you want to run your code in your CI/CD process. How does this work? When this is the case, you will want to setup a Headless Browser Runtime Environment. The following section will show you how to do this.

Headless Browser Runtime Environment

This section is going to show how to run your ClojureScript tests in JSDOM. This is something we want to configure so we can run our tests as part of our CI/CD process. The why of it is this: we don't need a browser GUI to run in our CI Environment. The GUI is slow and performance intensive. For this reason, we should consider setting up our tests to run in a headless environment. Here are some example of headless JS environments

For our purposes we are going to use jsdom which is a node based browser like environment. My preference here is because jsdom is a fast, healthy open source project backed with great documentation and battle tested. Evidence? companies like Gitlab, Facebook and Airbnb support their extensive testing infrastructure with jsdom.

The following sub-sections will walk through the steps required to setup this environment and teach figwheel to execute your tests inside of it.

Step 1 Add JavaScript Dependencies

Since we are going to be using packages from JavaScript land we need to setup our ClojureScript with a package.json file. To do this we can run this from the root of our nike-app project:

yarn init -y

The above will create a package.json file for us and a yarn.lock. The next step is to install jsdom:

yarn add -D jsdom

The above will take a short while and when complete your package.json will have been updated and you will have a node_modules dir in your repo. Your project should now look like this:

nike-app
├── README.md
├── deps.edn
├── dev.cljs.edn
├── package.json  # new
├── yarn.lock     # new
├── node_modules/ # new
├── resources
│   └── public
│       ├── index.html
│       └── style.css
├── src
│   └── nike
│       └── nike_app.cljs
└── test
    └── nike
        ├── test_runner.cljs
        └── nike_app_test.cljs

Step 2 Setup a JSDOM Environment

Now we need to write some JavaScript. Inside of test/nike create a file called test_environment.js and make it look like this:

// ------------------------------------------------------------
// Browser Test Environment - JSDOM
// ------------------------------------------------------------

const fs = require('fs');
var vm = require('vm');
var path = require('path');
const jsdom = require ('jsdom');

const args = process.argv.slice(2)
const testScript = args[0];
const url = args[1];

// helpers
function addScript (fname, window){
  var scriptText =  fs.readFileSync(fname, { encoding: "utf-8" });
  var scriptEl = window.document.createElement("script");
  scriptEl.text = scriptText;
  window.document.body.appendChild(scriptEl);
}

// setup JSDOM options
const {JSDOM, VirtualConsole} = jsdom;

const html = '<!DOCTYPE html>';

const virtualConsole = new VirtualConsole()

const options = {
  // see https://github.com/jsdom/jsdom#loading-subresources
  url:  url,
  resources: "usable",
  runScripts: "dangerously",
  pretendToBeVisual: true,
  // see https://github.com/jsdom/jsdom#virtual-consoles
  virtualConsole: virtualConsole.sendTo(console)
};

// create JSDOM instance
const dom = new JSDOM(html, options);

global.window = dom.window;

// run tests
addScript(testScript, dom.window);

The above is just a JavaScript file meant to be run by node. You can prove this by manually running the file like this:

node test_environment.js file.js url

file.js is any JavaScript file you have. When you call the above, it will run file.js in the jsdom environment.

In our case, file.js is going to be the name of the file that ClojureScript compiled. The tests will run inside of jsdom and the results of those tests will be returned to us.

To help understand the above script a little more:

  • addScript a function that is going to write our file to the jsdom environment which is what makes the script run
  • VirtualConsole is a jsdom config that allows the output to be returned to the console. For example, if you write (js/console.log "hi") in your tests, this line is what will return it to the console.
  • url this allows us to support figwheel HMR. When you make changes to your code, figwheel broadcasts those. We need this URL in there to tell our jsdom environment about the changes.

Alright, from here we are done with JavaScript and we can go back to configuring our ClojureScript project.

Step 3 Setup A Headless Test Runner

We need to create a second test runner which is going to be used to run our headless tests. So create a new file called test_runner_headless.cljs inside of tests/nike and make it look like this:

(ns nike.test-runner-headless
  (:require
    [figwheel.main.testing :refer-macros [run-tests-async]]
    ; tests here
    [nike.nike-app-test]))

(defn -main [& args]
  (run-tests-async 10000))

This file is the entrypoint for our tests. It's this file that will be run in jsdom. The next step is to write the code that will run our jsdom JS environment

Step 4 JS Environment Runner

Figwheel does this great thing where it allows us to choose the JS environment where you want to run your code.

In the browser testing section we let figwheel choose which JS environment to run our code in. Now we want to do something different. We want to tell figwheel the JS environment to run our tests in. In order to do this we need to write a Clojure file. Yup, we are writing Clojure.

Create a new file in test called nike.clj and add the following code:

(ns nike
  (:require
    [clojure.java.shell :as shell]))

(defn run-js-test-environment
  [{:keys [output-to open-url] :as args}]
  (let [js-env "test/nike/test_environment.js"
        result (shell/sh "node" js-env output-to open-url)]
    (if (zero? (:exit result))
      result
      (do (println result)
          (System/exit 0)))))

When we call run-js-test-environment it is ultimatley responsible for running node test_environment.js file.js url like we illustrated in Step 2 Setup a JSDOM Environment.

Awesome. At this point we have our dependencies, a JS environment and a test runner. The last step is to configure figwheel.

Step 5 Configure Figwheel

The first step is adding a new build configuration for figwheel. We cannot use our old dev.cljs.edn one because that is running our app/tests in the browser environment. However, because we need to tell figwheel to run this build in a different environment, we need a new build.

My rule of thumb is that every time you need to run your ClojureScript in a different JS environment, whether that is node or browser, you need a different build configuration. Create a new file called test.headless.cljs.edn in the root of our project and make it look like this:

^{:launch-js nike/run-js-test-environment}
{:main nike.test-runner-headless}

Now from here we need to create an alias in our deps.edn file:

{:paths
 ["src" "test" "resources" "target"]

 :deps
 {org.clojure/clojurescript {:mvn/version "1.10.866"}

  com.bhauman/figwheel-main  {:mvn/version "0.2.13"}

  reagent                   {:mvn/version "1.1.0"}}

 :aliases
 {:dev
  {:main-opts ["-m"  "figwheel.main" "--build" "dev" "--repl"]}

  :test-headless ; new alias
  {:main-opts ["--main"         "figwheel.main"
               "--compile-opts" "test.headless.cljs.edn"
               "--main"         "nike.test-runner-headless"]}}}

That's it. We are ready to run our headless browser testing setup. Try it out by running clj -M:test-headless in your terminal.

If it all worked you should see something like this:

# ...

Testing nike.nike-app-tests

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
Tests passed!

You are going to notice that you have to re-run this every time you want to run your headless tests. This is to be expected. The thing with this approach is that we have only really configured it to be run by our CI environment and also as a quick sanity check for newcomers to our project. Could we make it do more? Yup! You can take this and connect it to figwheel's file watching mechanism and have it behave exactly like the browser testing setup.

In addition, while I have chosen to use jsdom you could choose to use any headless JS environment you like!

Alternative Tools #

Up until now, i've shown you only cljs.test. The reason is because it does everything you need, it's a simple library and it's commonly used. However, for many reasons, you may be interested in using a different tool. That's totally possible with our setup! Part of the reason I broke down cljs.test into smaller groups of functionality:

  • assertion library
  • test definitions
  • test reporting
  • test runner

Is because some libraries you come across may only focus on one of these tasks and some may focus on more. Thus, it's helpful to know how each works at a lower level so you can confidently replace them if you need to. With this in mind, here are some alternatives you can consider if you want to experiment with different tools.

To replace your assertion libraries and test definitions these are some popular libraries in Clojure(Script)

For now I recommend sticking with cljs.test and then look into one of the above when/if the need arises.

And replacements for test-runners and test reporters

And you can even lean on the JavaScript ecosystem if you like. For example, I was able to get Jest working with ClojureScript.

What is my recommendation? Keep it simple, friends. Start with cljs.test. Find something you don't like and figure out exactly why it doesn't work for you and then, and only then, start looking for other tooling. For example, let's say you want better test reports because they really don't provide enough information. When that happens, start to look around for other tooling. For me, it's about being pragmatic with our choices.

Next Steps #

The goal here was to layout some context. Point you to tools and explore how and what to configure in your tooling for testing ClojureScript. From here, play with the setup, see how far it takes you and run with it. Here are some other resources you might be interested in:

Finally, there is no "right" or "wrong" testing tool. Start. Figure out what works and what doesn't work and then iterate. Have fun!