Skip to content

End to end testing example

JR Heard edited this page Sep 23, 2016 · 6 revisions

End to end testing example

You should be familiar with clojure.test, async tests in cljs.test, doo, fixtures, CORS, and ideally Component.

Our objective is to run ClojureScript frontend tests that communicate with a Clojure backend system through the network (e.g. through HTTP). If our system is comprised of a Clojure REST API and a ClojureScript SPA, it would be an End to End test.

Clojure Setup (backend)

From Clojure (clojure.test) we will:

  1. Start the Clojure system with allowing CORS requests.
  2. Compile the ClojureScript tests and run them pointing to the Clojure backend.
  3. Stop the Clojure system.

Steps 1 and 3 are done in a fixture, and step 2 is the end-to-end-suite.

(ns my-app.end-to-end-test
  (:require [clojure.test :refer :all]
            [doo.core :as doo]
            [cljs.build.api :as cljs]
            ;; These will be used in the following steps
            [com.stuartsierra.component :as component]
            [my-app.system :as system]
            [ring.middleware.cors :refer [wrap-cors]]))

(deftest end-to-end-suite
  (let [compiler-opts {:main 'my-app.e2e-runner
                       :output-to "out/test.js"
                       :output-dir "out"
                       :asset-path "out"
                       :optimizations :none}]
    ;; Compile the ClojureScript tests
    (cljs/build (apply cljs/inputs ["src/cljs" "test/cljs" "test/cljs-app-config"]) compiler-opts)
    ;; Run the ClojureScript tests and check the result
    (is (zero? (:exit (doo/run-script :phantom compiler-opts {}))))))

If the backend system uses Stuart Sierra's component we can start it using component/start. Alternatively, if you were using Jetty you could start it using (.start jetty-server) and stop it with (.stop jetty).

(use-fixtures :each
  (fn [f]
    ;; 1. Start the backend system at port 3333
    (let [system (-> (system/new-system {:http {:port 3333}
                                         :app {:middleware [(fn [handler]
                                                              (wrap-cors handler
                                                                         :access-control-allow-origin [#".*"]
                                                                         :access-control-allow-methods [:get :put :post :delete]))]}})
                     (assoc :nrepl {}) ;; Mock unnecessary dependencies before start
                     component/start)]
      ;; Perform post start setup here, like adding a test user to a database
      (f) ;; 2. run the test, i,e. end-to-end-suite
      (component/stop system)))) ;; 3. Stop the backend system

Why CORS is necessary

In production, the compiled js files are served from a url and communicate with that same url (i.e. www.yourgreatsite.com). This is not true when testing with doo where the compiled js files are grabbed from the file system. Then the JavaScript files are loaded from fs while the REST endpoints are in localhost. For security reasons, a JavaScript file is only allowed to communicate with the url that served it unless CORS is allowed. wrap-cors from Ring CORS is a ring middleware that allows CORS requests, and in general should not be allowed in production.

ClojureScript Setup (frontend)

In the frontend, we need to point ClojureScript to the test Clojure system's url, make some network requests, and check everything works as expected.

We point the network requests to the test system's url (same port). To have different base-url for production and testing we can have two different my-app.config files, one that is required for the test build (env/test/my_app/config.cljs) and the other for the production build (env/prod/my_app/config.cljs):

(ns my-app.config)

(def base-url "http://localhost:3333")

We add the usual doo runner detailing which namespaces have end-to-end tests:

(ns my-app.e2e-runner
  (:require [doo.runner :refer-macros [doo-tests]]
            [my-app.e2e-test]))

(doo-tests 'my-app.e2e-test)

The ClojureScript app requests data from the backend service and renders it (using reagent):

(ns my-app.main
  (:require [reagent.core :as r]
            [ajax.core :refer [GET]]
            [my-app.config]))

(def state (r/atom :loading))

(defn main-view []
  (case @state
    :loading [:div#loading "Loading..."]
    :started [:div#done "Done"]))

(defn render-app [target]
  (r/render [main-view] target)
  (GET (str config/base-url "/data") {:handler (fn [data] (reset! state :started))}))

The ClojureScript tests start the app (with render-app) and check after one second if the network call suceeded and the right dom element was loaded:

(ns my-app.e2e-test
  (:require [cljs.test :refer-macros [async deftest is testing use-fixtures]]
            [reagent.core :as reagent]
            [my-app.main :as main]
            [cljs-react-test.simulate :as sim]
            [cljs-react-test.utils :as tu]))

(def container (atom nil))

(use-fixtures :each
  {:before #(async done
              (reset! container (tu/new-container!))
              (done))
   :after #(tu/unmount! @container)})

(deftest start-the-app
  (testing "First view is loading"
    (is (= :loading @main/state))
    (is (some? (.getElementById js/document "loading"))))
  (async done
    ;; Start the app
    (main/render-app @container)
    (.setTimeout js/window
                 (fn []
                   (testing "Then the app is started"
                     (is (= :started @main/state)))
                     (is (some? (.getElementById js/document "done")))
                     (done))
                 1000)))
```

## Running the tests

Having all that setup, to run the tests we use `lein test`.