This is a stupid simple guide to setting up clojurescript tests. This was written out of sheer frustration at the lack of a simple guide to run cljs tests. This will not be a guide about the various configurations you can run tests with, or the various ways to run cljs tests or any of the fancy stuff. This is a guide I wished I had when I was trying to setup clojurescript tests. This is a dumb, copy-paste kinda guide to get you up and running as fast as possible. Because once you’ve set it up for a dummy project, setting it up for bigger projects gets easier.

I will take you from an empty project to setting clojurescript tests to run on github actions.

TLDR: here is the link to the code

EDIT

@thheller pointed out that a barebones project can be generated using npx. If you want to do that then here’s the command

npx create-cljs-project cljs-testing-with-lein

and then you can jump to this

note: this generates a slightly different project structure.

with npx

├── package-lock.json
├── package.json
├── shadow-cljs.edn
└── src
    ├── main
    └── test

with lein

├── package-lock.json
├── package.json
├── project.clj
├── shadow-cljs.edn
├── src
│   └── cljs_testing_with_lein
└── test
    └── cljs_testing_with_lein

In hindsight I should’ve gone with the npx command. This tutorial would’ve been much shorter.

Using Leiningen

You need to have Node (with npm) installed. You have it? good, now start a new project.

lein new cljs-testing-with-lein

I am deliberately starting with a barebones project, so you can see how you might add cljs support to a project that started off as clj.

Now run this to create a package.json file so you can import node modules.

npm init -y

To compile clojurescript we’ll use shadow-cljs (give it a star while you’re at it) because shadow-cljs is the most popular tool for cljs according to the clojure 2022 survey

Let’s add shadow-cljs as a dev dependency.

npm install -D shadow-cljs

(the -D flag indicates it is a dev dependency) you should see this in your package.json file

{
  "name": "cljs-testing-with-lein",
  "version": "1.0.0",
  "description": "A Clojure library designed to ... well, that part is up to you.",
  "main": "index.js",
  "directories": {
    "doc": "doc",
    "test": "test"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "shadow-cljs": "^2.19.6"
  }
}

you need to create a shadow-cljs.edn file.

npx shadow-cljs init

open the shadow-cljs.edn file, it looks like this

{:source-paths
 ["src/dev"
  "src/main"
  "src/test"]

 :dependencies
 []

 :builds
 {}}

you’ll need to edit the :source-paths to this

{:source-paths
 ["src/"
  "test/"]}

because our project structure doesn’t have a src/dev or a src/main or a src/test. Our structure is like this-

├── `package.json`
├── project.clj
├── resources
├── `shadow-cljs.edn`
├── src
│   └── cljs_testing_with_lein
├── target
│   └── stale
└── test
    └── cljs_testing_with_lein

Writing a cljs test

Let’s create a cljs test file called clojurescript_test.cljs

(ns cljs-testing-with-lein.clojurescript-test
  (:require
    [cljs.test :refer [deftest is]]))

(deftest a-test
  (is (= 1 2)))

That’s it, that’s the only cljs test we will write.

Setting up the Cljs-test runner

This is the hard part, the raison d’etre of this post.

We need to fiddle with shadow-cljs.edn a little, we need to add a test build.

:builds {:test {:target     :karma
                 :output-to  "out/test.js"
                 :ns-regexp  "-test$"
                 :autorun    true}}

:ns-regexp is a way to tell shadow-cljs which namespaces will have tests. "-test$" means that look at all namespaces the end in -test our test namespace cljs-testing-with-lein.clojurescript-test ends with -test so we are good to go.

Now, what about karma? karma is a bitch simple tool that allows you to execute JavaScript code in a browsers. Karma will be our test runner.

To get Karma do good run the following command

npm install -D karma karma-chrome-launcher karma-cljs-test

Okay, we are done pulling in dependencies.

You need to add a configuration for karma in karma.conf.js this is a very important step, if you don’t do it, your tests won’t run.

module.exports = function (config) {
    config.set({
        browsers: ['ChromeHeadless'],
        basePath: 'out', // this is the same as the base-path of `:output-to` in `shadow-cljs.edn`
        files: ['test.js'], // this is the same as the file-name (ending with .js) of `:output-to` in `shadow-cljs.edn`
        frameworks: ['cljs-test'],
        plugins: ['karma-cljs-test', 'karma-chrome-launcher'],
        colors: true,
        logLevel: config.LOG_INFO,
        client: {
            args: ["shadow.test.karma.init"],
            singleRun: true
        }
    })
};

One last thing, open package.json make this change

"scripts": {
  "test": "shadow-cljs compile test && karma start --single-run"
},

This is what your final package.json will look like

{
  "name": "cljs-testing-with-lein",
  "version": "1.0.0",
  "description": "A Clojure library designed to ... well, that part is up to you.",
  "main": "index.js",
  "directories": {
    "doc": "doc",
    "test": "test"
  },
  "scripts": {
    "test": "shadow-cljs compile test && karma start --single-run"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "karma": "^6.4.0",
    "karma-chrome-launcher": "^3.1.1",
    "karma-cljs-test": "^0.1.0",
    "shadow-cljs": "^2.19.6"
  }
}

Running cljs tests

Finally, lets run the tests!

npm run test

drum roll

this is the output I got

> cljs-testing-with-lein@1.0.0 test
> shadow-cljs compile test && karma start --single-run

shadow-cljs - config: /Users/abhinavomprakash/Documents/programs/clojure/cljs-testing-with-lein/shadow-cljs.edn
[:test] Compiling ...
[:test] Build completed. (63 files, 2 compiled, 0 warnings, 3.62s)
16 07 2022 16:22:47.546:INFO [karma-server]: Karma v6.4.0 server started at http://localhost:9876/
16 07 2022 16:22:47.548:INFO [launcher]: Launching browsers ChromeHeadless with concurrency unlimited
16 07 2022 16:22:47.557:INFO [launcher]: Starting browser ChromeHeadless
16 07 2022 16:22:48.056:INFO [Chrome Headless 103.0.5060.114 (Mac OS 10.15.7)]: Connected on socket wf6TE-HpJM03pF2_AAAB with id 24381769
LOG: 'Testing cljs-testing-with-lein.clojurescript-test'
Chrome Headless 103.0.5060.114 (Mac OS 10.15.7) cljs-testing-with-lein.clojurescript-test a-test FAILED
	FAIL in   (a-test) (cljs_testing_with_lein/clojurescript_test.cljs:7:7)
	expected: (=
	            1
	            2)
	  actual: (=
	            1
	            2)
	    diff: - 1
	          + 2

Chrome Headless 103.0.5060.114 (Mac OS 10.15.7): Executed 1 of 1 (1 FAILED) (0 secs / 0 secs)
TOTAL: 1 FAILED, 0 SUCCESS

You can fix the test if you like.

Setting up cljs testing with github actions

Github already has a clojure action to run clojure tests. You will need node to run cljs tests. So go to github actions and select the clojure action.

Your file will look like this

name: Clojure CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Install dependencies
      run: lein deps
    - name: Run tests
      run: lein test

you just need to add the following to the file.

- uses: actions/setup-node@v3
- name: run cljs tests
  run: npm run test

This is what the final file looks like

name: Clojure CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
    - name: Install dependencies
      run: lein deps && npm install

    - name: run clj tests
      run: lein test
    - name: run cljs tests
      run: npm run test

and push!

You’re pretty much done at this point.

Some pointers

  • If you try to run npm run test and see output like this
[:test] Compiling ...
[:test] Build completed. (63 files, 2 compiled, 0 warnings, 3.98s)
16 07 2022 17:41:42.213:INFO [karma-server]: Karma v6.4.0 server started at http://localhost:9876/

check that you have created a karma.conf.js and it is configured properly

  • If you have a .cljc file, you can write tests for it in a .cljc file and it will run for cljs and clj.
  • Be cautious to not use :refer :all in your :require because cljs doesn’t have support for it.
  • When writing tests for cljc files, separate out imports for cljs and clj with reader conditionals because you might import a macro and you’ll get an error(yes macros need to be tested too). here’s an example
(:require
    [clojure.test :refer [deftest is testing]]
    #?(:clj [my-project.core :refer [fn-foo macro-foo]]
       :cljs [my-project.core :refer-macros [macro-foo] :refer [fn-foo]]))
  • if you get an error like this Invalid :refer, var my-ns.core/foo does not exist and you know that foo exists, check if it is a macro and if it is you need to use :refer-macros

Other tools to look at

  1. cljs-test-runner
  2. lein-cljsbuild
  3. It’s useful to have a look at the official docs of shadow-cljs.

If you find any mistakes, have suggestions, or questions, tweet at me @the_lazy_folder I respond to all tweets.