January 31, 2016

Testing With Boot

In Building On Boot, I gave some high level benefits we'd found with Boot, compared to Leiningen, and how it had helped up streamline our build process. That article closed with a note about Boot not having the equivalent of common Leiningen plugins, and that's what I'm going to cover here, since that was the first real obstacle we encountered.

We use Jay Fields' Expectations library very heavily for most of our testing needs. We use clojure.test only for our Clojure-powered WebDriver testing. Leiningen has a test task built-in and we had been using lein-expectations for years. It was quite a shock to find out that Boot has no testing tasks built-in!

Boot's standard for driving clojure.test is Adzerk's boot-test. Using it in your build.boot file is as simple as adding a dependency on [adzerk/boot-test "1.0.7" :scope "test"] and then referring in the test task:

(merge-env! :dependencies '[[adzerk/boot-test "1.0.7" :scope "test"]])
(require '[adzerk.boot-test :refer [test]])

Now you can do boot test and run any tests in any of the namespaces in your source paths. Unfortunately there was no equivalent for Expectations so this was my first chance to roll up my sleeves and write a Boot task as a standalone project. The result is boot-expectations. Add a dependency on [seancorfield/boot-expectations "1.0.5" :scope "test"] and then refer in the expectations task:

(merge-env! :dependencies '[[seancorfield/boot-expectations "1.0.5" :scope "test"]])
(require '[seancorfield.boot-expectations :refer [expectation]])

Now you can do boot expectations to run any Expectations tests in any of the namespaces in your source paths. Do boot expectations -h to see all the options the task provides.

I relied very heavily on two sources for this project: Adzerk's boot-test for the shape of the code and the #boot channel on the Clojurians Slack where Boot's maintainers hang out and are extremely attentive and helpful! In particular, Micha Niskin was invaluable, answering all my newbie questions and making suggestions. Boot's "pods" made it easy to specify the version of Clojure to use when running the tests, without affecting the version of Clojure used for anything else in the build process (we have always run our tests against the released version we are actually using as well as the latest snapshot of Clojure's master branch so we don't get surprised by any changes being introduced in the next release). The "pod" machinery also made it straightforward to have namespaces required into the testing environment, and shutdown functions run after the tests, again without affecting the main build process. And all in a single JVM process that runs for the duration of the build.

As you can see on Boot's wiki, the ecosystem of community-maintained Boot tasks is already pretty strong and growing all the time.

Back to our build process and one of the key drivers for looking at Boot in the first place: we'd hit around 30K lines of production Clojure and 10K lines of test Clojure code, and we had it in three fair-sized projects with Leiningen. We wanted to reorganize the code and break it up into many more projects in order to have more flexibility in how we deploy code as well as being able to develop and test smaller chunks of code in isolation. We also wanted to be able to "pin" versions of certain libraries that we depended on across multiple "projects". With Leiningen we'd had multiple project.clj files and were already finding that we were pushing the declarative envelope of Leiningen by having to escape executable code into our defproject form. It felt like we were starting to fight the build tool. The declarative nature of project.clj didn't feel natural for the more fluid approach we wanted to take with our code base. What appealed about Boot was the possibility of a single build.boot file that could mix'n'match source and test code from various locations to allow the more modular develop / test approach we were aiming for, while still being able to easily build, push, and depend on artifacts from sub-projects. With Leiningen, we had a project for our WebDriver tests and it depended on a JAR built from the main source code project, so we would build and install (locally) a JAR of our main project, as part of the build. With Boot and a single build.boot file it was much easier to set up execution and test "contexts" as tasks that built the list of dependencies and source paths needed for each testing task. We now have all our Clojure code as "sub-projects" but can treat it as a single project too where that is more convenient.

In the next post, I'll take a diversion from World Singles' use of Boot and look at the Boot new project I've been working on for a month -- another "missing piece of the ecosystem" that I wanted to help fill!

Tags: clojure boot expectations