Search code examples
unit-testingclojuremockingringhttp-kit

How to mock test POST requests with body as JSON using ring mock request?


I am using http-kit as the server with wrap-json-body from ring.middleware.json to get the stringified JSON content sent from the client as the request body. My core.clj is:

; core.clj
; .. 
(defroutes app-routes
    (POST "/sign" {body :body} (sign body)))

(def app (site #'app-routes))

(defn -main []
    (-> app
        (wrap-reload)
        (wrap-json-body {:keywords? true :bigdecimals? true})
        (run-server {:port 8080}))
    (println "Server started."))

When I run the server using lein run the method works correctly. I am stringifying the JSON and sending it from the client. The sign method gets the json correctly as {"abc": 1}.

The problem is when during mock test. The sign method gets a ByteArrayInputStream and I am using json/generate-string to convert to string which fails in this case. I tried wrapping the handler in wrap-json-body but it is not work. Here are my test cases I tried out core_test.clj:

; core_test.clj
; ..
(deftest create-sign-test
    (testing "POST sign"
        (let [response
          (wrap-json-body (core/app (mock/request :post "/sign" "{\"username\": \"jane\"}"))
            {:keywords? true :bigdecimals? true})]
            (is (= (:status response) 200))
            (println response))))

(deftest create-sign-test1
    (testing "POST sign1"
        (let [response (core/app (mock/request :post "/sign" "{\"username\": \"jane\"}"))]
            (is (= (:status response) 200))
            (println response))))

(deftest create-sign-test2
    (testing "POST sign2"
        (let [response (core/app (-> (mock/body (mock/request :post "/sign")
                                       (json/generate-string {:user 1}))
                                     (mock/content-type "application/json")))]
            (is (= (:status response) 200))
            (println response))))

(deftest create-sign-test3
(testing "POST sign3"
    (let [response
      (wrap-json-body (core/app (mock/request :post "/sign" {:headers {"content-type" "application/json"}
                  :body "{\"foo\": \"bar\"}"}))
        {:keywords? true :bigdecimals? true})]
        (is (= (:status response) 200))
        (println response))))

All of the fails with the following error:

Uncaught exception, not in assertion.
expected: nil
  actual: com.fasterxml.jackson.core.JsonGenerationException: Cannot JSON encode object of class: class java.io.ByteArrayInputStream: java.io.ByteArrayInputStream@4db77402

How can I pass a JSON string as the body to the method in ring mock test?


Solution

  • There are three issues in your code.

    1. Your test doesn't wrap your app handler in wrap-json-body so it might not get correctly parsed request body in your handler. You need to first wrap your app in wrap-json-body and then call it with your mock request. (You could also have your app handler to be already wrapped instead of wrapping it both in your main function and tests)

      (let [handler (-> app (wrap-json-body {:keywords? true :bigdecimals? true})]
        (handler your-mock-request))
      
    2. Your mock request doesn't include proper content type and your wrap-json-body won't parse your request body to JSON. That's why your sign function gets ByteArrayInputStream instead of parsed JSON. You need to add content type to your mock request:

      (let [request (-> (mock/request :post "/sign" "{\"username\": \"jane\"}")
                        (mock/content-type "application/json"))]
        (handler request))
      
    3. Verify that your sign function returns a response map with JSON as string in body. If it creates response body as input stream you need to parse it in your test function. Below I am using cheshire to parse it (converting JSON keys to keywords):

      (cheshire.core/parse-stream (-> response :body clojure.java.io/reader) keyword)
      

    Additionally instead of writing your JSON request body by hand you can use Cheshire to encode your data into JSON string:

    (let [json-body (cheshire.core/generate-string {:username "jane"})]
      ...)
    

    With those changes it should work correctly like in my slightly modified example:

    (defroutes app-routes
      (POST "/echo" {body :body}
        {:status 200 :body body}))
    
    (def app (site #'app-routes))
    
    (let [handler (-> app (wrap-json-body {:keywords? true :bigdecimals? true}))
          json-body (json/generate-string {:username "jane"})
          request (-> (mock/request :post "/echo" json-body)
                      (mock/content-type "application/json"))
          response (handler request)]
      (is (= (:status response) 200))
      (is (= (:body response) {:username "jane"})))