I recently had the need to stub HTTP endpoints in Clojure. I needed something that could do this on the HTTP level since in one case I was integrating with a Java library that used Apache HTTP Client under the covers and another that used Axis 2. I tried searching for such a library in Clojure and even asked on stackoverflow but I couldn’t find any. In the Java ecosystem we have for example mockwebserver and while I probably could use this, its API is a bit cumbersome to use from Clojure. So my first reaction was to create a small wrapper library on top of mockwebserver. But mockwebserver brings with it quite a lot of dependencies and is a bit too focused on mocking than what I typically require when writing these kind of tests. I wanted a library that acted more like a fake or stub (see this article for a discussion on terminology) with fewer dependencies so I decided to base it on nanohttpd instead. The project is called (simply) Stub HTTP and is available on github and clojars.
Example
This example demonstrates how to use the with-routes! macro of Stub HTTP (there’s also a start! function if you prefer that) with clojure test and clj-http-lite. This example returns the json document `{ “hello” : “world” }` if the request path matches “/something”:
(ns stub-http.example
(:require [clojure.test :refer :all]
[stub-http.core :refer :all]
[cheshire.core :as json]
[clj-http.lite.client :as client]))
(deftest Example
(with-routes!
{"/something" {:status 200 :content-type "application/json"
:body (json/generate-string {:hello "world"})}}
(let [response (client/get (str uri "/something"))
json-response (json/parse-string (:body response) true)]
(is (= "world" (:hello json-response))))))
This starts the stub server on a free random port and shuts it down automatically after the assertion. As you can see we supply a map as argument to `with-routes!` followed by a body. The map is referred to as a route map which consists of entries made up by a request specification (key) and a response specification (value). If you look closely you’ll see that we make a request to resource `(str uri “/something”)` where `uri` is a property generated and exposed by the macro and points to the root path of the server (for example `http://localhost:31212`). There are other ways of constructing the request- and response specification, for example:
(ns stub-http.example
(:require [clojure.test :refer :all]
[stub-http.core :refer :all]
[cheshire.core :as json]
[clj-http.lite.client :as client]))
(deftest AnotherExample
(with-routes!
{{:method :get :path "/something" :query-params {:name "value"}}
(fn [request]
{:status 200 :content-type "text/plain" :body (->> request :headers :something)})}
(let [response (:body (client/get ...))]
(is (= "xxx" response)))))
In this example we match only a `GET` request that has path equal to `/something` that also has a query parameter `name=value` and if so we return a response body that is constructed from a header supplied with the request. For more information please refer to the wiki.
Recordings
When stubbing an HTTP endpoint it can be useful to see which request generated which response and what these requests and responses actually looked like. For this reason Stub HTTP records all requests and its associated response. This can be retrieved from the `routes` atom available in the server record exposed by the macro. For example:
(ns stub-http.example
(:require [stub-http.core :refer :all]
[clj-http.lite.client :as client]))
(with-routes! { "/something" {:status 200 :content-type "text/plain" :body "body"} }
(client/get ...)
(:routes server))
The `routes` atom consists of a vector of maps that (among other things) contains the “recordings” in the `:recordings` key:
{:recordings [{:request :response ]}
This means that each route has zero to many recordings that matched the request specification of that route. The `request` has the following schema:
{:method
:headers
and the response is defined like this:
{:status
:headers
:content-type
:body }
The `server` instance (from which routes where extracted in the example above) contains the convenience functions called `recorded-requests` and `recorded-responses` that allows you to get all recorded requests and/or responses for this server instance. For example:
(ns stub-http.example
(:require [stub-http.core :refer :all]
[clojure.test :refer :all]
[clj-http.lite.client :as client]))
(with-routes! { "/something" {:status 200 :content-type "text/plain" :body "body"}}
(client/get ...)
(let [responses (recorded-responses server)]
; Now do something with the responses, for example count them
(is (= 1 (count responses)))))
See this link for more info.
Summary
While there are great alternative solutions in the Clojure space such as clj-http-fake and ring-mock they target a specific library and doesn’t work on the HTTP level. In some cases this is not enough and you need to be able to easily create fake/stub HTTP resources in your tests. Stub HTTP is my humble attempt to make it easy to do exactly this. So hopefully this library can become useful for someone in the same situation as myself. Comments, issues, pull requests or other kinds of feedback is of course welcome. Happy testing 🙂