I have been using Pedestal for RESTful API servers and its endpoint unit testing. This approach is to set the server up and test it on endpoint level. The so-called "endpoint unit testing" is well documented in below page.
http://pedestal.io/reference/unit-testing#_testing_your_service_with_response_for
This time, however, I am using Lacinia-Pedestal for GraphQL (not RESTful, in other words) and am wondering if I can apply the same endpoint testing logic. In Lacinia-Pedestal repository (https://github.com/walmartlabs/lacinia-pedestal), I could not find a relevant instruction. In fact it doesn't mention about unit testing at all.
If anyone has experience with this approach, can you please share?
Thanks!
-- edit
I am adding my testing code here.
resources/main-schema.edn:
{:queries
{:hello
{:type String}}}
core.clj
(ns lacinia-pedestal-lein-clj.core
(:require [clojure.edn :as edn]
[clojure.java.io :as io]
[com.walmartlabs.lacinia.pedestal2 :as p2]
[com.walmartlabs.lacinia.schema :as schema]
[com.walmartlabs.lacinia.util :as util]
[io.pedestal.http :as http]))
(defn resolve-hello
[_ _ _]
"hello, darren")
(defn hello-schema
[]
(-> (io/resource "main-schema.edn")
slurp
edn/read-string
(util/inject-resolvers {:queries/hello resolve-hello})
schema/compile))
(def service
(-> (hello-schema)
(p2/default-service nil)
http/create-server
http/start))
and Pedestal (not Lacinia-Pedestal) says that the server instance can be set up and tested by the following code snippet:
(ns lacinia-pedestal-lein-clj.core-test
(:require [lacinia-pedestal-lein-clj.core :as core]
[clojure.test :as t]
[io.pedestal.http :as http]
[io.pedestal.test :as ptest]
[com.walmartlabs.lacinia.pedestal2 :as p2]
[com.walmartlabs.lacinia.util :as util]))
(def service
(:io.pedestal.http/service-fn
(io.pedestal.http/create-servlet service-map)))
(is (= "Hello!" (:body (response-for service :get "/hello"))))
But, I believe this way works for RESTful but not for GraphQL because GraphQL needs to set schema (.edn file) and resolvers for a server.
So, I tried to tweak this.
(ns lacinia-pedestal-lein-clj.core-test
(:require [lacinia-pedestal-lein-clj.core :as core]
[clojure.test :as t]
[io.pedestal.http :as http]
[io.pedestal.test :as ptest]
[com.walmartlabs.lacinia.pedestal2 :as p2]
[com.walmartlabs.lacinia.util :as util]))
(defonce service
(-> (core/hello-schema)
(p2/default-service nil)
http/create-server
http/start))
(t/is (= "Hello!"
(:body
(util/response-for service
:get "/hello")))) ;; NOT WORKING
But it does not work this way because response-for expects interceptor-service-fn type.
So, as far as I know, the real question is how to use response-for function with GraphQL server instance.
in fact, i found a solution from Lacinia documentation.
https://lacinia.readthedocs.io/en/latest/tutorial/testing-1.html
Related
Assuming I have some kind of router set up that maps some routes to handlers something like this...
(ns myapp.user.api
(:require [reitit.core :as r]))
; define handlers here...
(def router
(r/router
[["/user" {:get {:name ::user-get-all
:handler get-all-users}}]
["/user/:id"
{:post {:name ::user-post
:handler user-post}}
{:get {:name ::user-get
:handler user-get}}]]))
And those handlers then call services that want access to the routing information...
(ns myapp.user-service
(:require [myapp.user.api :as api]))
; how can I get access to the route properties inside here..?
(defn get-all-users [])
(println (r/route-names api/router)))
When I try to import the router from the api file, into the service, I get a problem with circular dependencies, because the api requires handler, which requires service, so service can not then require api.
What's the best way to avoid this circular dependency? Can I look up values and properties of the router from within services?
I use six general approaches to avoid circular dependencies in clojure. They all have different tradeoffs and some situations one will fit better than another. I list them in order from what I prefer most to what I prefer least.
I show one example for each below. There may be more ways I haven't thought of, but hopefully this gives you some ways of thinking about the issue.
Refactor the code to remove the commonly referenced vars into a new namespace and require that namespace from both original namespaces. Often this is the best and simplest way. But can't be done here because the root handler var is a literal containing a var from the other namespace.
Pass in the dependent value into the function at runtime so as to avoid having to require the namespace literally.
(ns circular.a)
(defn make-handler [routes]
(fn []
(println routes)))
(ns circular.b
(:require [circular.a :as a]))
(def routes
{:handler (a/make-handler routes)})
;; 'run' route to test
((:handler routes))
Use multimethods to provide the dispatch mechanism, and then defmethod your binding from the other namespace.
(ns circular.a
(:require [circular.b :as b]))
(defmethod b/handler :my-handler [_]
(println b/routes))
(ns circular.b)
(defmulti handler identity)
(def routes
{:handler #(handler :my-handler)})
(ns circular.core
(:require [circular.b :as b]
;; now we bring in our handlers so as to define our method implementations
[circular.a :as a]))
;; 'run' route to test
((:handler b/routes))
Use a var literal that is resolved at runtime
(ns circular.a)
(defn handler []
(println (var-get #'circular.b/routes)))
(ns circular.b
(:require [circular.a :as a]))
(def routes
{:handler a/handler})
;; 'run' route to test
((:handler routes))
Move the code into the same namespace.
(ns circular.a)
(declare routes)
(defn handler []
(println routes))
(def routes
{:handler handler})
;; 'run' route to test
((:handler routes))
Use state. Store one of the values in an atom at runtime.
(ns circular.a
(:require [circular.c :as c]))
(defn handler []
(println #c/routes))
(ns circular.b
(:require [circular.a :as a]
[circular.c :as c]))
(def routes
{:handler a/handler})
(reset! c/routes routes)
((:handler routes))
(ns circular.c)
(defonce routes (atom nil))
You are making a simple mistake somewhere. My example:
(ns demo.core
(:use tupelo.core)
(:require
[reitit.core :as r]
[schema.core :as s]
))
(defn get-all-users [& args] (println :get-all-users))
(defn user-post [& args] (println :user-post))
(defn user-get [& args] (println :user-get))
; define handlers here...
(def router
(r/router
[
["/dummy" :dummy]
["/user" {:get {:name ::user-get-all
:handler get-all-users}}]
["/user/:id"
{:post {:name ::user-post
:handler user-post}}
{:get {:name ::user-get
:handler user-get}}]
]))
and use here:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test)
(:require
[clojure.string :as str]
[reitit.core :as r]
))
(dotest
(spyx-pretty (r/router-name router))
(spyx-pretty (r/route-names router))
(spyx-pretty (r/routes router))
)
with result:
*************** Running tests ***************
:reloading (demo.core tst.demo.core)
Testing _bootstrap
-----------------------------------
Clojure 1.10.3 Java 15.0.2
-----------------------------------
Testing tst.demo.core
(r/router-name router) =>
:lookup-router
(r/route-names router) =>
[:dummy]
(r/routes router) =>
[["/dummy" {:name :dummy}]
["/user"
{:get
{:name :demo.core/user-get-all,
:handler
#object[demo.core$get_all_users 0x235a3fc "demo.core$get_all_users#235a3fc"]}}]]
Ran 2 tests containing 0 assertions.
0 failures, 0 errors.
based on my favorite template project
I keep getting "Invalid anti-forgery token" when wrapping specific routes created with metosin/reitit reitit.ring/ring-router. I've also tried reitit's middleware registry, but it didn't work too. Although I could just wrap the entire handler with wrap-session and wrap-anti-forgery, that defeats the reitit's advantage on allowing route-specific middleware.
(ns t.core
(:require [immutant.web :as web]
[reitit.ring :as ring]
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
[ring.middleware.content-type :refer [wrap-content-type]]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.session :refer [wrap-session]]
[ring.util.anti-forgery :refer [anti-forgery-field]]
[ring.util.response :as res]))
(defn render-index [_req]
(res/response (str "<form action='/sign-in' method='post'>"
(anti-forgery-field)
"<button>Sign In</button></form>")))
(defn sign-in [{:keys [params session]}]
(println "params: " params
"session:" session)
(res/redirect "/index.html"))
(defn wrap-af [handler]
(-> handler
wrap-anti-forgery
wrap-session
wrap-keyword-params
wrap-params))
(def app
(ring/ring-handler
(ring/router [["/index.html" {:get render-index
:middleware [[wrap-content-type]
[wrap-af]]}]
["/sign-in" {:post sign-in
:middleware [wrap-af]}]])))
(defn -main [& args]
(web/run app {:host "localhost" :port 7777}))
It turns out that metosin/reitit creates one session store for each route (refer to issue 205 for more information); in other words, ring-anti-forgery is not working because reitit does not use the same session store for each route.
As of the time of this answer, the maintainer suggests the following (copied from the issue for ease of reference within Stack Overflow):
mount the wrap-session outside of the router so there is only one instance of the mw for the whole app. There is :middleware option in ring-handler for this:
(require '[reitit.ring :as ring])
(require '[ring.middleware.session :as session])
(defn handler [{session :session}]
(let [counter (inc (:counter session 0))]
{:status 200
:body {:counter counter}
:session {:counter counter}}))
(def app
(ring/ring-handler
(ring/router
["/api"
["/ping" handler]
["/pong" handler]])
(ring/create-default-handler)
;; the middleware on ring-handler runs before routing
{:middleware [session/wrap-session]}))
create a single session store and use it within the routing table (all instances of the session middleware will share the single store).
(require '[ring.middleware.session.memory :as memory])
;; single instance
(def store (memory/memory-store))
;; inside, with shared store
(def app
(ring/ring-handler
(ring/router
["/api"
{:middleware [[session/wrap-session {:store store}]]}
["/ping" handler]
["/pong" handler]])))
Not shown in this answer is the third option that the maintainer calls for PR.
I am using ring adapter jetty server in my compojure api project. Now I want to add ring middleware CORS to my Project. How should I add and where should I add ring middleware CORS in my project?
These are my project code snippets
API
(ns clojure-dauble-business-api.core
(:require [compojure.api.sweet :refer :all]
[ring.util.http-response :refer :all]
[clojure-dauble-business-api.logic :as logic]
[clojure.tools.logging :as log]
[clojure-dauble-business-api.domain.artwork]
[cheshire.core :as json])
(:import [clojure_dauble_business_api.domain.artwork Artwork]))
(defapi app
(GET ["/hello/:id", :id #"[0-9]+"] [id :as request]
(log/info "Function begins from here" request)
(def jsonString (json/generate-string (get-in request [:headers])))
(log/info "Create - header value is " (get-in (json/parse-string jsonString true) [:accesstoken]))
(def artworkData (logic/artwork-id (->> id (re-find #"\d+") Long/parseLong)))
(def data (if (not-empty artworkData)
{:data artworkData :status 200}
{:data [] :status 201}))
(ok data)))
Method to Run the api on Jetty
(ns clojure-dauble-business-api.routes
(:require [compojure.core :refer :all]
[ring.adapter.jetty :as jetty]
[ring.middleware.cors :refer [wrap-cors]]
(clojure-dauble-business-api [core :as core]
[test :as t])))
(def app
(routes core/app t/test))
(jetty/run-jetty app {:port 3000})
Here core/app is the above API function path and t/test another API function(I have not provided the code here).
I've a luminus project with some simple compojure-api routes.
I've added carmine to communicate with a redis server, using the wcar* macro (defined in services.clj) to make calls to it, and everything works fine.
Now I'm trying to add some tests, but seems that the redis connection doesn't works properly during them, because I'm receiving this error with lein test:
ERROR Carmine connection error
clojure.lang.ExceptionInfo: Carmine connection error {}
Since it's working in dev e prod environments, I think that is something related to a missing env load in the test environment, but I didn't find a way to solve it.
These are relevant parts of the code in use:
test.clj
(ns app.test.handler
(:require [clojure.test :refer :all]
[ring.mock.request :refer :all]
[app.handler :refer :all]))
(deftest test-app
(testing "redis ping"
(let [response ((app) (request :get "/api/redis-ping"))]
(is (= 200 (:status response))))))
services.clj
(ns app.routes.services
(:require [ring.util.http-response :refer :all]
[compojure.api.sweet :refer :all]
[schema.core :as s]
[app.config :refer [env]]
[clojure.tools.logging :as log]
[mount.core :refer [defstate]]
[taoensso.carmine :as car :refer (wcar)]))
(defmacro wcar* [& body] `(car/wcar
{:spec {:host (:redis-host env) :port (:redis-port env)}}
~#body))
(defapi service-routes
(context "/api" []
:tags ["myapi"]
(GET "/redis-ping" []
:return String
:summary "A redis client test."
(ok (wcar* (car/ping "hello"))))))
handler.clj
(ns app.handler
(:require [compojure.core :refer [routes wrap-routes]]
[app.routes.services :refer [service-routes]]
[compojure.route :as route]
[app.env :refer [defaults]]
[mount.core :as mount]
[app.middleware :as middleware]))
(mount/defstate init-app
:start ((or (:init defaults) identity))
:stop ((or (:stop defaults) identity)))
(def app-routes
(routes
#'service-routes
(route/not-found
"page not found")))
(defn app [] (middleware/wrap-base #'app-routes))
Profiles.clj
{:profiles/dev {:env {:redis-host "127.0.0.1" :redis-port 6381}}
:profiles/test {:env {:redis-host "127.0.0.1" :redis-port 6381}}}
Config.clj
(ns app.config
(:require [cprop.core :refer [load-config]]
[cprop.source :as source]
[mount.core :refer [args defstate]]))
(defstate env :start (load-config
:merge
[(args)
(source/from-system-props)
(source/from-env)]))
SOLUTION
Add a text fixture with the mount/start command that's executed before tests.
Add to test.clj:
(defn my-test-fixture [f]
(mount/start)
(f))
(use-fixtures :once my-test-fixture)
You are using mount to manage your application state lifecycle. I think you are not calling (mount/start) in your tests thus your app.config/env state is not initialized properly. On the other hand when you start your application (mount/start) is probably called and thus it's working correctly.
I'm writing a Compojure application and am using clj-webdriver to graphically test it. I'm trying to use with-redefs to mock out the function that pulls out data from persistence to just return canned values, but it's ignoring my function overwrite. I know with-redefs works in terms of vars, but it's still not working:
project.clj relevant pieces:
(defproject run-hub "0.1.0-SNAPSHOT"
:main run-hub.handler/start-server)
handler.clj:
(ns run-hub.handler
(:require [compojure.core :refer :all]
[compojure.handler :as handler]
[compojure.route :as route]
[ring.adapter.jetty :refer :all]
[run-hub.controllers.log-controller :as log-controller]))
(defroutes app-routes
(GET "/MikeDrogalis/log" [] (log-controller/mikes-log))
(route/resources "/")
(route/not-found "Not Found"))
(def app (handler/site #'app-routes))
log-controller.clj:
(ns run-hub.controllers.log-controller
(:require [run-hub.views.log :as views]
[run-hub.persistence :as persistence]))
(defn mikes-log []
(views/mikes-log (persistence/mikes-log)))
persistence.clj
(ns run-hub.persistence
(require [clj-time.core :as time]
[run-hub.models.log :as log]))
(defn mikes-log [] [])
And finally, my graphical test - which tries to override mikes-log and fails:
(fact
"It has the first date of training as August 19, 2012"
(with-redefs [persistence/mikes-log (fn [] (one-week-snippet))]
(to (local "/MikeDrogalis/log"))
(.contains (text "#training-log") "August 19, 2012"))
=> true)
Where one-week-snippet is a function that returns some sample data.
(defn start-server []
(run-jetty (var app) {:port 3000 :join? false}))
I am able to use with-redefs in a clj-webdriver test doing the following:
(defn with-server
[f]
(let [server (run-jetty #'APP {:port 0 :join? false})
port (-> server .getConnectors first .getLocalPort)]
(binding [test-port port]
(try
(println "Started jetty on port " test-port)
(f)
(finally
(.stop server))))))
(use-fixtures :once with-server)
Then the whole bunch of tests gets its own jetty and this seems to run in
such a manner that with-redefs works.