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 thatfoo
exists, check if it is a macro and if it is you need to use:refer-macros
Other tools to look at
- cljs-test-runner
- lein-cljsbuild
- 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.