OnyCloud

Testing Web Applications with clj-webdriver

17 August 2011 by Hong Jiang

At OnyCloud, testing is an important part of our development activities. Trakr, our issue tracking and project management app, has about 250 tests and nearly 1000 assertions. Those are all tests for the server code. However, for a JavaScript-heavy web application, only testing the server side covers at most half of the code base. The robustness of programs written in dynamic languages, JavaScript in particular, relies on testing. Therefore, we spent significant amount of time looking for good web testing solutions.

We used to use watir-webdriver, which is a nice Ruby library to drive browsers, but we had to write the tests in Ruby. Since we use Clojure as the main language, we prefer to write our functional tests in Clojure too. clj-webdriver is a Clojure library for driving a web browser using Selenium-WebDriver as the backend. The project README provides detailed documentation. As an example for its usage in tests, the following test checks that the proper error message is shown when a user tries to register with an invalid email address.

(deftest invalid-email
  (let [b *browser*]
    (get-url b "http://localhost:8887/trakr/")
    (-> b
        (find-it {:href "/signup"})
        click)
    (wait-until-present (find-it b {:name "email"}))
    (-> b
        (find-it {:name "email"})
        (input-text "tester@localhost"))
    (-> b
        (find-it {:type "submit"})
        click)
    (wait-until-present
     (find-it b {:id "flash-error" :text #"Invalid email"}))))

When we trigger an action, we do not know how long it takes for the JavaScript code to execute and produce the expected effects. The helper macro wait-until-present makes it easy to wait for some element to become visible on the page:

(defmacro wait-until-present [find-it-form
                              & {:keys [timeout] :or {timeout 5}}]
  `(wait-until
    (and (exists? ~find-it-form)
         (visible? ~find-it-form)) :timeout ~timeout))

wait-until is a more general macro to wait for some predicate to become true. It repeatedly evaluates the predicate until it is true. When the predicate evaluates to false, the macro waits for an exponentially increasing interval, until the timeout is hit.

(defmacro wait-once [pred ms-to-wait]
  `(if (try ~pred (catch Exception _# false))
     true
     (do
       (Thread/sleep ~ms-to-wait) false)))

(defmacro wait-until [pred & {:keys [timeout] :or {timeout 5}}]
  `(let [ms# (* 1000 ~timeout)]
     (loop [interval# 1
            remaining# ms#]
       (when (<= remaining# 0) (throw (Exception. "Timeout reached!")))
       (when-not (wait-once ~pred interval#)
         (recur (* 2 interval#) (- remaining# interval#))))))