Continuous Coverage with Clojure

Zubair Haque
6 min readJul 16, 2018

In the past I worked on a back-end services team that geared their architectural approach to a microservices application design. The service I worked on was written in clojure which is basically a compiled JVM functional programming language, also something I was really not that familiar with until I started to work on this team. I had to get acclimated quick and bump up the test coverage. I needed a library that would accommodate what I was trying to accomplish, so I decided to go with clj-http for the API tests. clojure test functions have a BDD type setup, when you define a test function it lets you set a testing context as a string nested in your test function.

Getting Started

Since this is a clojure service I am working on, I will be using the leiningen tool to build code that I am developing, as a matter of fact it’s much simpler than Maven you can almost compare it to rake & bundler for Ruby. Leiningen will be helping us with the following:

Project compilation: Clojure gets converted into Java bytecode, and Leiningen tasks automate that process.Dependency management. Just like Ruby's bundler and gemfiles, Leiningen automates the process of resolving and downloading the Java jar files that your code depends on.Running tasks. Similar to Ruby's Rake, you can run custom automation tasks written in Clojure.Deployment. Helps with creating Java jars which can be executed or incorporated in other projects. Similar to Ruby gems & project.clj is similar to a gem file.

So let’s get started by adding dependencies to the project.clj file. Just as an FYI Clojure libraries are distributed the same way as other JVM languages in jar files.

(defproject com.something.servicename "0.1.0-SNAPSHOT"
:description "description of the service and what it is doing"
:url "http://companyname.com"
:min-lein-version "2.0.0"
:exclusions [org.clojure/clojure]
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/tools.reader "1.2.2"]
[org.clojure/test.check "0.7.0"]
[ring/ring-core "1.5.0"]
[ring/ring-jetty-adapter "1.5.0"]
[clj-http "3.3.0"]
[cheshire "5.6.3"]
[http-kit "2.2.0"]]

Listed below is a description of what was added:

test.check is a Clojure property based testing tool.

ring-core is the standard library used to write web apps.

ring-jetty-adapter is a ring adapter that uses Jetty.

clj-http is an HTTP library wrapping the Apache HTTPComponents client.

cheshire is a library that encodes and decodes JSON.

http-kit is supports concurrent asynchronous calls to a server.

Profiles

What are profiles?

Having a :dev profile & a :test profile defined in project.clj allows you to be flexible when specifying project specific development/testing tooling. You can add the key/value pairs in defproject.

(defproject com.something.servicename "0.1.0-SNAPSHOT"
:description "description of the service and what it is doing"
:dependencies [[dependency/name "1.0"]]
:profiles
{:dev {:dependencies
[http-kit.fake "0.2.1"]
[http-kit.fake "0.2.1"]
[ring/ring-mock "0.3.1"]]
:plugins [[some-plugin "0.3.10"]]
:resource-paths ["path/to/test-data"]
:test {:test-paths ["test"]
:dependencies [[org.clojure/test.check "0.7.0"]
[pjstadig/humane-test-output"0.8.1"]]

Test Definition

You can start adding defining your tests after you define your namespace:

(ns com.something.servicename.name-of-test
(:require [clojure.test.check.clojure-test :refer :all]
[cheshire.core :as json]
[clojure.java.io :as io]
[com.package.test.service.test-util :as util]
[clj-http.client :as client])
(:use [clojure.test]
[com.package.service.core.namespace]))

Every clojure source file starts with a namespace declaration. The ns macro is will help you define your imports & requires.

:require  setting up access to other Clojure namespaces from your code, you can optionally refer functions to the current ns:as It is common to make a namespace available under an alias

Let’s define a simple test with no arguments, I can also list an example where this test can also be called by other tests:

;; defining a simple test(deftest test-something-simple
(testing "A simple assertion"
(is (= 1 1))))
;; defining a helper function which your deftest function will call(defn helper-function [m ks expected]
(= expected (get-in m ks)))
(deftest test-that-calls-a-function
(testing "Calling a helper function in your test"
(is (ns/helper-function response-map [:key] "value"))))

Assertions

The is macro lets you make assertions:

(deftest some-test
"Description of test"
(is (= 4 (+ 2 2))))

so something neat I loved when I was using pytest was I could pass in an optional second argument, which is a string giving details about the assertion I am trying to make. Here is an example below:

self.assertEqual(200, response.status_code, 'I should get a 200 response code')

You can also accomplish the same thing clojure.test by doing the following:

(is (= 200 (:status user-information)) "I should get a 200 response code")

This can be very helpful when your test fails and the message shows up in the error report after runtime of your test suite.

Using get-in to validate the response body:

Once you get your JSON response returned from the request you make in your test function, your test might want to validate a nested key/value pair that is being returned. The first thing you should do is turn your JSON response to a map, which is what the cheshire library does by using json/parse-string:

(deftest test-turn-response-to-map
(testing "Making a request and turning the response to a map"
(let [request (client/get (str "/url")
{:headers {:header "value"}
:content-type :json})
response (-> request
:body
(json/parse-string true))])))

if you look at the above test function you can see that once the request is made the response object is then turned into a map. Now you can use get-in to retrieve the nested values in that map:

(deftest test-nested-key-value-pair
(testing "Validating the response body returned from your request"
(is (= "expected" (str (get-in response [:object :key]))))))

Test Selectors

Leiningen has a test task that lets you set metadata on tests so that you can filter a set of tests, basically tagging your tests. This allows you to run just a selection of your tests.

You can put a keyword argument when invoking them which will specify which test namespaces to run:

:test-selectors {:default #(not-any? % [:integration :http :db])
:db :db
:integration :integration
:acceptance :acceptance
:all
(constantly true)}

What’s happening here is all deftest forms which are not tagged with the metadata added will be skipped. Don’t feel like waiting for all of your integration tests to complete every time?

Flag those test methods with the following ^:acceptance metadata flag:

(ns com.something.servicename.name-of-test
(:require [clojure.test.check.clojure-test :refer :all]
[cheshire.core :as json]
[clojure.java.io :as io]
[com.project.test.service.util :as util]
[clj-http.client :as client])
(:use [clojure.test]
[com.project.service.ns.core]))
(deftest ^:acceptance test-name
(testing "A test that does something"
(is (= 1 1))))

Running the tests

Once you have written the test method, we needed to run the code on the command line:

$ lein test :acceptancelein test com.project.test.service.#####Ran 1 tests containing 1 assertions.0 failures, 0 errors.

this basically ran all of the tests in the given namespace, that were tagged with the ^:acceptance metadata flag. The result was printed summarizing the test results:

Running :only one test method:

$ lein test :only namespace/test-method-name

Analyzing Failures

There was one extra complication with automatic translation. If I have two expressions:

FAIL in (test-namespace-name) (namespace_test.clj:34)A user was unable to do something and you received an error messageexpected: (= expected-status status)actual: (not (= 200 500))

the expected line shows you what you were expecting the output to be and actual shows in the console output of what was actually returned from the test run.

The detailed information above should provide you with a firm understanding on how to develop individual clj-http test suites. it is important to consider how you organize and run those suites as part of your project’s build. You should now have a firm understanding of:

Profiles: Setting up your to development tools for your project. You can make a testing profile for your suite of tests.Test Selectors: Allows you to tag your tests. Specify a selection of your tests to run at a time.Project setup: Similar to Ruby's Rake, you can run custom automation tasks written in Clojure.Test Definition: Helps with creating Java jars which can be executed or incorporated in other projects. Similar to Ruby gems & project.clj is similar to a gem file.

I hope this is made your deep dive in the clojure.test & clj-http much easier.

--

--

Zubair Haque

The Engineering Chronicles: I specialize in Automated Deployments