Home

Awesome

Clojars Project

Testing your application's HTTP API endpoint involves coming up with a consistent ways to:

This is the kind of day-to-day grind that can turn into an unexpected time sink and lead to problems down the line if not done well.

This library does all that for you so that your tests can focus as much as possible on your applciation's business logic and behavior. Instead of writing tests that look like this:

(deftest get-book
  (is (= (->> (mock/request :get (str "/book/" (:book/id book)))
              handler
              :body
              slurp
              (mu/decode "application/transit+json"))
         #:book{:id 0, :title "East of Eden"})))

You can write tests that looks like this:

(deftest get-book
  (is (= (deth/response :get :book {:book/id 0})
         #:book{:id 0 :title "East of Eden"})))

endpoint-test takes care of the machinery of constructing requests, handling them using your application's handler, and reading responses. It's not going to like, change your life, but it'll make it a little nicer.

Example

endpoint-test is meant to be used with donut.system, and it relies on the presence of a ring handler component at [:http :handler] a reitit router component at [:routing :router].

(ns donut.endpoint-test.example
  (:require
   [clojure.test :refer [deftest is use-fixtures]]
   [donut.endpoint.router :as der]
   [donut.endpoint.test.harness :as deth]
   [donut.system :as ds]))

(use-fixtures :each (ds/system-fixture :test))

;; donut.endpoint.test.harness is meant to work with systems defined by
;; donut.system, and with systems

(defmethod ds/named-system :test
  [_]
  {::ds/defs
   {:http
    {:handler
     (ds/ref [:routing :ring-handler])}

    :routing
    {:ring-handler der/RingHandlerComponent
     :router       der/RouterComponent
     :router-opts  der/router-opts
     :routes       [;; returning multiple maps
                    ["/api/v1/books"
                     {:name :books
                      :get  {:handler (fn [_]
                                        {:status 200
                                         :body   [#:book{:id 0 :title "East of Eden"}
                                                  #:book{:id 1 :title "The Martian"}
                                                  #:book{:id 2 :title "Their Eyes Were Watching God"}]})}}]

                    ;; returning a single map
                    ["/api/v1/books/{book/id}"
                     {:name :book
                      :get  {:handler (fn [_]
                                        {:status 200
                                         :body   #:book{:id 0 :title "East of Eden"}})}
                      :put  {:handler (fn [{:keys [all-params]}]
                                        all-params)}}]]}}

   ::ds/plugins
   [deth/test-harness-plugin]})

(deftest get-books
  (is (-> (deth/response :get :books)
          (deth/contains-entity? #:book{:id 0 :title "East of Eden"}))))

(deftest get-book
  (is (= (deth/response :get :book {:book/id 0})
         #:book{:id 0 :title "East of Eden"})))

Usage

All examples assume (require [donut.endpoint.test.harness :as deth]).

Add this fixture to your tests:

(use-fixtures :each (ds/system-fixture :name-of-your-test-system))

This will start a system and bind it to donut.system/*system*, which endpoint-test relies on to get your applications ring handler and router.

If you need to modify your system for your test, you can also use the ds/with-*system* macro:

(deftest get-book
  (ds/with-*system* :test
    (is (= (deth/response :get :book {:book/id 0})
           #:book{:id 0 :title "East of Eden"}))))

Configuring your system

Add deth/test-harness-plugin to your system's plugins:

(defmethod ds/named-system :test
  [_]
  {::ds/defs 
   {}
   
   ::ds/plugins
   [deth/test-harness-plugin]})

This plugin configures your system, setting some default values that the library's helpers rely on to make your tests more concise. It tells the library where to find a router comonent and an http-handler component, and it adds a component that sets the default content type of requests to :transit-json (other values are :json and :html).

Testing requests and responses

endpoint-test offers the following helper functions for creating requests, handling them, and getting responses:

request

This constructs a ring request using ring.mock.request/request. It lets you construct a path using a route name, like so:

(request :get :books)             ;; could use the path "/api/v1/books"
(request :get :book {:book/id 1}) ;; could use the path "/api/v1/books/1"

It also set's the request's content type to whatever your application's :default-request-content-type is (see below.)

handle-request

Creates a request using request and passes it to the ring handler you've defined in your system.

(handle-request :get :books)

read-body

Decodes a response body, traqnsforming it from JSON or whatever back into Clojure.

(read-body (handle-request :get :books))

response

Composition of handle-request and read-body

(deftest get-book
  (is (= (deth/response :get :book {:book/id 0})
         #:book{:id 0 :title "East of Eden"})))

Using the plugin

endpoint-test relies on some configuration within your system to retrieve the components it needs to create request paths with a router and to pass requests to your ring handler. This configuration is provided by deth/test-harness-plugin:

(def test-harness-plugin
  {:donut.system.plugin/name
   ::test-harness-plugin

   :donut.system.plugin/doc
   "Configures system so that donut.endpoint.test.harness can find the
   components needed to construct and dispatch requests."

   :donut.system.plugin/system-defaults
   {::ds/registry {:donut/endpoint-router [:routing :router]
                   :donut/http-handler    [:http :handler]}
    ::ds/defs     {::config {:default-request-content-type :transit-json}}}})

The important part is :donut.system.plugin/system-defaults at the end. Whatever values you set in your system map will take precedence over these values.

Change the value of the component [::deth/config :default-request-content-type] to :json or :html or your own custom content-type if your endpoints return that content and not transit.

Development

Run tests

clojure -X:test