Pedestal

Unit testing

It is a good practice to separate core business logic from handler, and more generally, interceptor code. This separation of concerns has many benefits, including facilitating testing. However, there is still value in exercising the end-to-end operation of your service endpoints since, at minimum, it provides quick feedback that you’ve wired things up correctly. Pedestal provides the test helper response-for utility function to facilitate testing of services without having to spin up an http server.

Testing your service with response-for

The response-for helper function is specific to servlet-based services and simulates the receipt of a servlet request and creation of a servlet response. It takes a service function, verb and url as required parameters while headers and a request body are optional. Let us examine those parameters in more detail.

The service function refers to the :io.pedestal.http/service-fn key value assoc’d to the service map during service initialization.

The verb is an HTTP method represented as a keyword (i.e., :get, :post, :delete, etc…​).

The url is a relative url represented as a string.

The headers and request body inputs are optional and specified with the :headers and :body keys, respectively.

Usage

Before using response-for in tests, a test service must be created. This is done by calling io.pedestal.http/create-servlet with the service map as a parameter. The resulting service-fn can be bound to a var and used in subsequent tests.

(def service (:io.pedestal.http/service-fn (io.pedestal.http/create-servlet service-map)))

Testing GET

The following example illustrates a simple execution of response-for within a test:

(is (= "Hello!" (:body (response-for service :get "/hello"))))

The response returned by response-for contains :status, :body and :header keys. The value of the :headers key is a map with stringified keys. Testing header values would look something like this:

(is (= "text/plain"
       (get-in (response-for service :get "/hello") [:headers "Content-Type"])))

Testing POST

POST’ing to a service endpoint can be tested by using the :post verb and specifying a request :body. The route under test typically includes the body-params interceptor to support request payload parsing. Therefore, you will need to set the Content-Type header of the test request to the appropriate value based on the payload format of the request body.

(is (= 200 (:status (response-for service
                                  :post "/foo"
                                  :headers {"Content-Type" "application/json"}
                                  :body "{\"foo\":\"bar\"}"))))

Notice how Content-Type is a string.

Testing with Query and path parameters

When testing routes which take path and/or query parameters, it is recommended to create a url-for test helper based on your routes and use that to create the relative url for your tests.

(require '[io.pedestal.http.route :as http.routes])

(def routes #{["/user/:id/items" :post `user-items]}

(def url-for (http.routes/url-for-routes (http.routes/expand-routes routes))))

(is (= 200 (:status (response-for service
                                  :get (url-for ::user-items
                                                 :path-params {:id 1}
                                                 :query-params {:sort "ASC"})))))

Testing Async interactions

Nothing special needs to be done when testing routes which include async interactions. The response-for helper forces asynchronous request processing to synchronous processing for test purposes.