clojure.spec is a standard, expressive, powerful, and integrated system for specification and testing. It lets you define the shape of your data, and place contraints on it. Once the shape, and constraints are defined, clojure.spec can then generate sample data which you can use to test your functions. In this post I’ll walk you through how you can use clojure.spec in conjunction with other libraries to write unit tests.
Motivation
As developers, we are accustomed to writing example-based tests - we provide a known input, look at the resulting output, and assert that it matches our expectations. Although there is nothing wrong with this approach, there are a few drawbacks:
- It is expensive as it takes longer to complete.
- It is easier to miss out on the corner cases.
- It is more prone to pesticide paradox.[1]
In contrast, clojure.spec allows you to do generative, property-based testing. Generative testing allows you to specify what kind of data you are looking for. This is done by using generators. A generator is a declarative description of possible inputs to a function.[2] Property-based testing allows you to specify how your program is supposed to behave, given an input. A property is a high-level specification of behavior that should hold for a range of inputs.[3]
Setup
Creating an App
We’ll begin by creating an app using lein
and defining the dependencies. So go ahead and execute the following to create your project:
1 | lein new app clj-spec |
Adding Dependencies
Next we’ll add a few dependencies. cd
into clj-spec
and open project.clj
. Add the following to your :dependencies
1 | [org.clojure/clojure "1.8.0"] |
clojure.spec
comes as a part of Clojure 1.9 which, as of writing, isn’t out yet. If you’re on Clojure 1.8, as I am, you can use clojure-future-spec
which will give you the same APIs. circleci/bond
is a stubbing library which we’ll use to stub IO, network calls, database calls, etc. cloverage
is the tool we’ll use to see the coverage of our tests.
Using clojure.spec
Simple Specs
Fire up a REPL by executing lein repl
and require
the required namespaces ;)
1 | clj-spec.core=> (require '[clojure.spec.alpha :as s]) |
spec
will let us define the shape of our data, and constraints on it. gen
will let us generate the sample data.
Let’s write a simple spec which we can use to generate integers.
1 | clj-spec.core=> (s/def ::n int?) |
We’ve defined a spec ::n
which will constrain the sample data to only be integers. Notice the use of double colons to create a namespace-qualified symbol; this is a requirement of the spec library. Now let’s generate some sample data.
1 | clj-spec.core=> (gen/generate (s/gen ::n)) |
s/gen
takes a spec as an input and returns a generator which will produce conforming data. gen/generate
exercises this generator to return a single sample value. You can produce multiple values by using gen/sample
:
1 | clj-spec.core=> (gen/sample (s/gen ::n) 5) |
We could have done the same thing more succinctly by using the in-built functions as follows:
1 | clj-spec.core=> (gen/generate (gen/vector (gen/int) 5)) |
Spec-ing Maps
Let’s say we have a map which represents a person and looks like this:
1 | {:name "John Doe" |
Let’s spec this.
1 | (s/def ::name string?) |
We’ve defined ::name
to be a string, and ::age
to be an integer (positive or negative). You can make your specs as strict or as lenient as you choose. Finally, we define ::person
to be a map which requires the keys ::name
and ::age
, albiet without namespace-qualification. Let’s see this in action:
1 | clj-spec.core=> (gen/generate (s/gen ::person)) |
By now you must have a fair idea of how you can spec your data and have sample values generated that match those specs. Next we’ll look at how we can do property-based testing with specs.
Using test.check
test.check
allows us to do property-based testing. Property-based tests make statements about the output of your code based on the input, and these statements are verified for many different possible inputs.[4]
A Simple Function
1 | (defn even-or-odd |
We’ll begin by testing the simple function even-or-odd
. We know that for all even numbers we should get :even
and for all odd numbers we should get :odd
. Let’s express this as a property of the function. Begin by require
-ing a couple more namespaces.
1 | clj-spec.core=> (require '[clojure.test.check :as tc]) |
Now for the actual property.
1 | (def property |
We have a generator which will create a vector of 0s and 1s only. We pass that vector as an input to our function. Additionally, we know that the number of 0s should equal the number of :even
s returned and that the number of 1s should equal the number of :odd
s returned.
Next, let’s test this property.
1 | clj-spec.core=> (tc/quick-check 100 property) |
Awesome! We ran the test a 100 times and passed. The added benefit is that the input generated will be different every time you run the test.
Using bond
bond
is a library which will let you stub side-effecting functions like database calls. We’ll require the namespce and modify our code to save even numbers to database.
First, the namespace.
1 | clj-spec.core=> (require '[bond.james :as bond]) |
Next, the code.
1 | (defn save |
Now let’s update the property and stub save
.
1 | (def property |
Notice how we’re using bond/with-stub
and telling it to stub save
function which calls the database. Later, we assert that the number of times that the databse was called is equal to the number of evens in the vector. Let’s verify the property.
1 | clj-spec.core=> (tc/quick-check 10000 property) |
Voilà! It works!
The last part of this post is about finding out test coverage using cloverage
. For that, we’ll be moving our code to core.clj
and writing test under the test
directory.
Using cloverage
To see cloverage
in action, we’ll need to add our functions to core.clj
. Here’s what it’ll look like:
1 | (ns clj-spec.core |
Update your clj-spec.core-test
to the following:
1 | (ns clj-spec.core-test |
Here we are using the defspec
macro to run the same property-based test a 100 times only this time we’ll run the test via command-line using lein
. Execute the following command to run the test and see the coverage.
1 | lein run -m cloverage.coverage -t 'clj-spec.core-test' -n 'clj-spec.core' |
This will make use of cloverage
to run the tests. -t
denotes our test namespace and -n
denotes the namespace for whom the tests are written. You’ll get an output like this:
1 | ... |
Perfect!! Now we know how much coverage we have. The HTML file has a nice graphical representation of which lines we’ve covered with our tests.
Conclusion
This brings us to the end of the post on using clojure.spec
to write generative, property-based tests in both the REPL and source files. Generative testing automates the task of having to come up with examples for your tests. Where to go from here? Each of these libraries is pretty powerful in itself and will provide you with the necessary tools to write powerful, robust, and expressive tests that require minimal effort. So, head over to the offical docs to learn more.