How to test your servlet-based service

Use the response-for test helper to facilitate testing servlet-based services. It is recommended to create a url-for test helper in order to construct test requests, particularly when query or path parameters are involved.

Reference: Unit Testing

(ns how-to-test.service-test
  (:require [clojure.test :refer :all]
            [how-to-test.service :as service]
            [io.pedestal.test :refer :all]
            [io.pedestal.http :as http]
            [io.pedestal.http.route :as route]
            [cheshire.core :as json]))

;; Create the service under test
(def service
  "Service under test"
  (::http/service-fn (http/create-servlet service/service)))

;; Create the test url generator
(def url-for
  "Test url generator."
  (route/url-for-routes (route/expand-routes service/routes)))

(deftest service-test
  ;; test GET'ing a simple response body
  (is (= 200
         ;; Use the route name with `url-for` to generate the url
         (:status (response-for service :get (url-for ::service/handler1)))))
  ;; test GET with path and query params
  (is (= 200
         (:status (response-for service :get (url-for ::service/handler2
                                                      :path-params {:foo "bar"}
                                                      :query-params {:sort "ASC"})))))
  ;; test POST'ing JSON
  (is (= 200
         (:status (response-for service
                                :post (url-for ::service/handler3)
                                ;; Set the `Content-Type` so `body-params`
                                ;; can parse the body
                                :headers {"Content-Type" "application/json"}
                                ;; Encode the payload
                                :body (json/encode {:foo "bar"})))))

  ;; testing File upload
  ;; We'll create a form body which simulates uploading two files.
  ;; NOTE: Consider using a test-friendly store during your testing.
  ;; See for an example.
  (let [form-body (str "--XXXX\r\n"
                       "Content-Disposition: form-data; name=\"file1\"; filename=\"foobar1.txt\"\r\n\r\n"
                       "Content-Disposition: form-data; name=\"file2\"; filename=\"foobar2.txt\"\r\n\r\n"
        {:keys [status]} (response-for service
                                       :post (url-for ::service/handler4)
                                       :body form-body
                                       :headers {"Content-Type" "multipart/form-data; boundary=XXXX"})]
    ;; testing file processing
    (is (= 201 status))))

How to Handle Errors

Add an :error implementation to your interceptor(s) or create a purpose-built, error handling interceptor. If you can’t handle the exception in your error handler, re-attach it to the context using the io.pedestal.interceptor.chain/error key so that other interceptors have an opportunity to handle it.

Reference: Error Handling

(ns error-handling.service
  (:require [io.pedestal.http :as http]
            [io.pedestal.log :as log]
            [io.pedestal.interceptor :as interceptor]
            [io.pedestal.interceptor.chain :as chain]
            [io.pedestal.interceptor.error :as error]
            [io.pedestal.http.route :as route]
            [io.pedestal.http.body-params :as body-params]
            [ring.util.response :as ring-resp]))

(def throwing-interceptor
  (interceptor/interceptor {:name ::throwing-interceptor
                            :enter (fn [ctx]
                                     ;; Simulated processing error
                                     (/ 1 0))
                            :error (fn [ctx ex]
                                     ;; Here's where you'd handle the exception
                                     ;; Remember to base your handling decision
                                     ;; on the ex-data of the exception.
                                     (let [{:keys [exception-type exception]} (ex-data ex)]
                                       ;; If you cannot handle the exception, re-attach it to the ctx
                                       ;; using the `:io.pedestal.interceptor.chain/error` key
                                       (assoc ctx ::chain/error ex)))}))

(def service-error-handler
  (error/error-dispatch [ctx ex]
                        ;; Handle `ArithmeticException`s thrown by `::throwing-interceptor`
                        [{:exception-type :java.lang.ArithmeticException :interceptor ::throwing-interceptor}]
                        (assoc ctx :response {:status 500 :body "Exception caught!"})

                        :else (assoc ctx ::chain/error ex)))

(def common-interceptors [service-error-handler (body-params/body-params) http/html-body])

(def routes #{["/" :get (conj common-interceptors throwing-interceptor)]})

(def service {:env                     :prod
              ::http/routes            routes
              ::http/resource-path     "/public"
              ::http/type              :jetty
              ::http/port              8080
              ::http/container-options {:h2c? true
                                        :h2?  false
                                        :ssl? false}})

How to serve static resources

Add the :io.pedestal.http/resource-path key to the service map and set your project’s resource path to the root resource directory.

If your resources directory is resources then :io.pedestal.http/resource-path "/public" will instruct Pedestal to look for static content in the resources/public directory. Thus, http://localhost:8080/foo.txt would serve resources/public/foo.txt.

If a resource is not found and no route matches the url, then a 404 is returned.

Reference: Service Map

(def service {:env                     :prod
              ::http/routes            routes
              ;; Resources will be served from the resource directory's `public`
              ;; sub directory.
              ::http/resource-path     "/public"
              ::http/type              :jetty
              ::http/port              8080
              ::http/container-options {:h2c? true
                                        :h2?  false
                                        :ssl? false}})