Skip to content
brentonashworth edited this page Jan 24, 2012 · 13 revisions

Testing

The file src/lib/clj/one/test.clj contains the namespace one.test which implements support for testing ClojureScript code.

Clojure already has good testing frameworks, such as clojure.test, Midje and Lazytest. ClojureScript has a protocol for evaluation with implementations for the browser and Rhino. We can combine these two things to create a great testing environment for ClojureScript.

Let's experiment with this from the ground up. Start a Clojure REPL with lein repl and then start the development server.

lein repl
(dev-server)

Instead of connecting to the browser from a REPL, let's do something different. Let's create a browser evaluation environment which will allow us to evaluate arbitrary JavaScript code in the browser from Clojure.

(use '[cljs.repl :only (-setup -tear-down -evaluate)])
(use '[cljs.repl.browser :only (repl-env)])
(def eval-env (repl-env))
(-setup eval-env)

We have created the environment and will now need to connect to it. We could open a browser and navigate to the app but that's boring. Let's do it from the REPL.

(use '[clojure.java.browse :only (browse-url)])
(browse-url "http://localhost:8080/development")

We should now have an active connection. Let's test it by sending some JavaScript for evaluation.

(-evaluate eval-env "example.clj" 1 "1 + 1;")
;=> {:status :success, :value "2"}

The call to -evaluate takes the evaluation environment, a file name, line number and JavaScript string and returns a map containing the results of the evaluation.

What we would really like to be able to do is evaluate ClojureScript forms in this environment. We can use a function defined in ClojureScript, in the cljs.repl namespace, to help with this.

(use '[cljs.repl :only (evaluate-form)])
(evaluate-form eval-env {:context :statement :locals {}} "example.clj" '(+ 1 1))
;=> "2"

evaluate-form takes the evaluation (run time) environment, a map which represents the (compile time) environment in which the form will be evaluated, the file name and the form to evaluate. It returns a string representing the result of evaluation which can be read with the Clojure reader.

We are working at a very low level here, but hopefully you are starting see where we are going with this. The one.test namespace builds on these capabilities to give us a more convenient way to do this.

(use '[one.test :only (evaluate-cljs cljs-eval *eval-env*)])
(evaluate-cljs eval-env '(+ 1 1))
;=> 2

evaluate-cljs is simpler to call and returns a Clojure value which we can use. We can also specify the namespace in which this form is to be evaluated.

(evaluate-cljs eval-env 'one.sample.model '@state)
;=> {:state :init}

In this example, we dereference the state atom in the one.sample.model namespace. The return value indicates that we are in the :init state.

The macro cljs-eval makes evaluating multiple forms in the same namespace a little easier, it will also ensure that the passed namespace has been loaded. Instead of passing in the evaluation environment, it relies on the value of the dynamic var eval-env. This allows the testing tool to determine which environment to run in.

(binding [*eval-env* eval-env]
  (cljs-eval one.sample.model (+ 1 1) @state))
;=> {:state :init}

As shown in the last example, multiple forms are passed to cljs-eval, only the return value for the last form will be returned.

Driving the browser

We now have the ability run arbitrary ClojureScript code in the browser from Clojure. Take a little break and reflect on how awesome that is. We'll wait.

This allows us to do some interesting things. If you have the form in view within the browser, try this.

(binding [*eval-env* eval-env]
  (cljs-eval one.sample.view
             (dispatch/fire [:editing-field "name-input"])
             (set-value! (by-id "name-input") "James")
             (dispatch/fire [:field-changed "name-input"] "James")))

This simulates the activity of a user filling in the form. First we fire the event that corresponds to the field gaining focus, then the value is entered and finally we fire the event that corresponds to the field losing focus, indicating that the value has been updated.

We might be tempted to use the .focus and .blur methods of the input field directly. However, in some browsers these events do not actually fire unless the browser window is the active window. Since we are presumably typing these commands into the REPL, it is the window containing our REPL that is active window, and not the browser. Here we are trying to make a more compelling demo, but this is something to be aware of when you are writing your automated tests.

Now let's click the button to submit the form.

(binding [*eval-env* eval-env]
  (cljs-eval one.sample.view
    (clojure.browser.dom/click-element :greet-button)))

To summarize, from Clojure we can evaluate arbitrary ClojureScript in the browser. This enables us to drive the browser, simulating user activity. Since we are running in the Clojure process which started the server, we can simultaneously simulate user interaction and test conditions on the client and server.

Example tests

ClojureScript One includes some example tests which demonstrate how to use this from the clojure.test testing framework.

Here is one example test:

(deftest test-enter-new-name
  (reset! *database* #{})
  (cljs-eval one.sample.view
             (dispatch/fire :init)
             (set-value! (by-id "name-input") "Ted")
             (fx/enable-button "greet-button")
             (clojure.browser.dom/click-element :greet-button))
  (cljs-wait-for #(= % :greeting) one.sample.model (:state @state))
  (is (= (cljs-eval one.sample.view (.innerHTML (first (nodes (by-class "name")))))
         "Ted"))
  (is (= (cljs-eval one.sample.view (.innerHTML (first (nodes (by-class "again")))))
         ""))
  (is (= (cljs-eval one.sample.model @state)
         {:state :greeting, :name "Ted", :exists false}))
  (is (true? (contains? @*database* "Ted"))))

This test will reset the *database* atom on the server, fill in and submit a form in the browser, wait until the :state key in the state atom on the client is :greeting, which indicates that the network activity has completed, and then test that the entered name appears on the visible web page, the state atom in the browser is correct and the *database* atom on the server contains the entered name.

See the file test/one/sample/test/integration.clj for an example of how to set up tests to open a browser and configure the evaluation environment.

Run lein test to run all the tests and see this in action.