Restpect provides a set of functions to write succint and readable integration tests over RESTful APIs.
This test:
(require '[clj-http.client :as http]
'[clojure.test :refer [deftest is]])
(deftest create-user
(let [res (http/put "http://example.com/api/v1/users/john"
{:content-type :json
:as :json
:form-params {:first-name "John"
:last-name "Doe"
:email "[email protected]"}})]
(is (= 201 (:status res)))
(is (= "John" (get-in res [:body :first-name])))
(is (= "Doe" (get-in res [:body :last-name])))
(is (= "[email protected]" (get-in res [:body :email])))
(is (integer? (get-in res [:body :user-id])))))
Can be rewritten with restpect like:
(require '[restpect.core :refer [expect]]
'[restpect.json :refer [PUT]]
'[clojure.test :refer [deftest]])
(deftest create-get-and-delete-user
(expect (PUT "http://example.com/api/v1/users/john" {:first-name "John"
:last-name "Doe"
:email "[email protected]"})
{:status 201
:body {:first-name "John"
:last-name "Doe"
:email "[email protected]"
:user-id integer?}}))
Or, using status shorthands:
(require '[restpect.core :refer [created]]
'[restpect.json :refer [PUT]]
'[clojure.test :refer [deftest]])
(deftest create-user
(created (PUT "http://example.com/api/v1/users/john" {:first-name "John"
:last-name "Doe"
:email "[email protected]"})
{:first-name "John"
:last-name "Doe"
:email "[email protected]"
:user-id integer?}))
Add the following to your project map as a dependency:
[restpect "0.2.0"]
The restpect.json
namespace provides wrappers around the clj-http
request functions with sane defaults for JSON API requests (coerce request and
response as JSON, don't throw exceptions on 4xx and 5xx responses, etc.).
All these functions have the following signature:
(POST url)
(POST url body)
(POST url body extras)
The body
is passed to clj-http as the :form-params
for POST
, PUT
, PATCH
and DELETE
, and as the :query-params
for GET
, HEAD
and OPTIONS
.
extras
is a map of overrides passed to the clj-http call.
The main assertion function is restpect.core/expect
:
(expect response spec)
The first argument (usually a clj-http response map, although it can be any value), will be compared against the given spec with the following criteria:
- For maps, compare the value of each key in the spec with the value at the same key
of the response, using
expect
recursively. - For sets, check that each element in the spec matches some element in the response,
comparing with
expect
recursively. This is useful to look for an element somewhere in a list, regardless of the position. - For other collections, compare each element in the spec with the same element at the
same position in the response, using
expect
recursively. - For functions, pass the value in the response to the spec function expecting a truthy result.
- For Regular expressions match the spec with the actual value (using re-find).
- For the rest of the values, expect the spec and the response values to be equal.
Example:
(expect (GET url)
{:status 404
:body [{:result nil?
:code 125
:message #".+ not found"}]})
This assertion is equivalent to the following:
(let [res (GET url)]
(is (= 404 (:status res)))
(is (nil? (get-in res [:body 0 :result])))
(is (= 125 (get-in res [:body 0 :code])))
(is (re-matches #".+ not found" (get-in res [:body 0 :message]))))
As seen in the example, expect
is opinionated in the sense that it makes it
simple to test for values and conditions on specific fields of the reponses
rather than doing an exact comparison of the payloads.
restpect.core
also provides a set of wrappers around expect
with the
names of the different HTTP response status codes: ok
, created
, bad-request
,
not-found
, etc.
These helpers implicitly validate the :status
value of the given response map,
and can optionally take a second argument that will be compared against the
response body.
Using status shorthands, the example from the previous section becomes:
(not-found (GET url)
[{:result nil?
:code 125
:message #".+ not found"}])
Another example:
(deftest create-and-delete-user
(created (PUT "http://example.com/api/v1/users/john" {:email "[email protected]"}))
(ok (GET "http://example.com/api/v1/users/john"))
(ok (DELETE "http://example.com/api/v1/users/john"))
(not-found (GET "http://example.com/api/v1/users/john")))
Restpect also provides a custom test reporter that adds request and response
information to failure messages (provided by expect
) and does some formatting:
The report multimethod can be found in restpect.report/report
and can be used
with plugins that allow to override the test reporter, like
eftest
and lein-test-refresh:
;; project.clj
:eftest {:report restpect.report/report}
:test-refresh {:report restpect.report/report}
If you already work with a custom reporter and just want to add some
request/reponse data to its output, consider adding a defmethod for
:type :response
, for example:
(require '[restpect.report :refer [print-response]]
'[eftest.report.progress :as eftest]
'[clojure.test :refer [with-test-out]])
(defmulti report :type)
(defmethod report :default [m] (eftest/report m))
(defmethod report :response [m]
(with-test-out
(println "\r" (repeat 80 " ") "\033[F" "\033[F")
(print-response (:response m))))