Monday, September 10, 2012

Automated Browser-based Testing with Clojure

Automated Browser-based Testing with Clojure:
For some time at World Singles, we've been running a suite of Selenium-based tests as part of our automated build. We have been using the selenese Ant task and HTML-based test descriptions. We have 58 tests containing 337 commands and it takes about five and a half minutes to run that section of our build. We can also run most of the individual Selenium tests in the browser using the Selenium core HTML/JS tool. It's been working pretty well for us but there are a couple of sticking points.
The first issue is that we have a few end-to-end scenario tests that need to run through user-facing operations as well as admin-facing operations and these are on separate domains. That works fine using selenese via Ant but you can't run them individually in the browser, which makes constructing and debugging the tests a bit painful. And of course that HTML markup is painful to work with in the first place!
The second issue is that it can be very hard to set up the right test environment for some of these scenarios. For example, we want to test that if member A blocks member B, then member B can't send them messages. In order to test that properly, we need to ensure that the database doesn't have records for either member A or member B blocking each other (and, ideally, we'd like to remove the block after the test). You can do it via Selenium commands, navigating thru the site, but it's tedious and a bit error-prone (it's all too easy to leave test state around and affect subsequent tests in the suite, for example).
For a while we've wanted to drive the Selenium tests programmatically so that we could setup and tear down the necessary test data and do more sophisticated tests based on conditions and loops and so on. I started looking at Daniel Gregoire's clj-webdriver about a month ago and I liked the way tests looked using it, especially with a few helper functions to make the tests read more like English, and initially implemented a couple of our more troublesome Selenium tests using clj-webdriver and Jay Fields' Expectations, which we were already using for our unit tests elsewhere.
Unfortunately, because Expectations runs all of your tests on shutdown as a block, it was a little hard to control when the browser opened (and closed) and to break tests across multiple namespaces without having the browser start up for each namespace. Ideally, I wanted to be able to run tests individually in the REPL while developing them as well as run an entire suite with a single browser session.
The solution I came up with relies on some nice features of Leiningen and good ol' clojure.test.
First off, I created helper functions to start / stop the browser:
(ns testsuite.core
(:require [clj-webdriver.taxi :refer :all]))

(def ^:private browser-count (atom 0))

(defn browser-up
"Start up a browser if it's not already started."
[]
(when (= 1 (swap! browser-count inc))
(set-driver! {:browser :firefox})
(implicit-wait 60000)))

(defn browser-down
"If this is the last request, shut the browser down."
[& {:keys [force] :or {force false}}]
(when (zero? (swap! browser-count (if force (constantly 0) dec)))
(quit)))

This lets me call (browser-up) .. (browser-down) in each test - to run them individually - and (browser-up) .. (browser-down :force true) around the test suite to run all the tests in a single browser session.
Next, I created a project-specific Leiningen task with-browser as a higher-order task that would start a browser session, run any specified Leiningen tasks, and then shut the browser down:
(ns leiningen.with-browser
(:require [webdriver.core :refer [browser-up browser-down]]
[leiningen.core.main :as main]))

(defn ^:higher-order with-browser
"Run a (test) task with a browser already open."
[project task-name & args]
(browser-up)
(-> (Runtime/getRuntime)
(.addShutdownHook
(proxy [Thread] []
(run [] (browser-down :force true)))))
(main/apply-task (main/lookup-alias task-name project)
(dissoc project :eval-in)
args))

I can say lein with-browser test and run all my tests in a single browser session. This code lives in tasks/leiningen/with_browser.clj in my project and I had to add a .lein-classpath file to my project, containing :tasks.
See Project-specific Tasks in the Leiningen documentation for more detail.
Since I needed the task to share the full environment of the testing project, I also added :eval-in :leiningen to my project.clj file, which the higher-order task removes from the project before running the (test) task so that Leiningen will exit with a non-zero status if tests fail - required for Ant integration.
The browser is closed when the process (Leiningen) exits, via the shutdown hook that is added.

What does an individual test file look like? Something like this:
(ns testsuite.test.profile-scripts
(:require [clj-webdriver.taxi :refer :all]
[clojure.test :refer :all]
[testsuite.core :refer :all]))

(deftest edit-my-profile
(browser-up)
(login :eg "testuser" "secret")
(go-to :eg "/profile/edit")
(input-text "#aboutme" "I like long walks on the beach and being a snugglebunny.")
(click "#btnSaveProfile2")
(is (= "Your profile has been successfully updated." (text ".success p")))
(is (= "In order for your profile to appear to other members, please fill out the missing fields identified below." (text ".warning p")))
(browser-down))

The login and go-to functions are just conveniences (in testsuite.core) to login to a specified website with a given username and password (:eg represents eligiblegreeks.com) and go to a specific URL. This test verifies that if you make a change (on this test profile), you get the success message and you still get the partial profile message.
If this test is run standalone (e.g., loading it into the REPL and calling (run-tests) or (edit-my-profile)), it will open and close a browser (if none is open). This is convenient for developing and running individual tests either in files or directly in the REPL.
Since we can run any server-side code in our tests at this point, we can easily add code to a test to setup and tear down the test environment by adding / removing test data in our database either as part of the test itself or using the fixtures in Clojure's built-in testing library, clojure.test.
Finally, in our Ant build file, we simply have an exec task to run lein in the testsuite project folder with the arguments with-browser and test:
<target name="clj-selenium" depends="clj-ws-build" description="Run the Clojure-based Selenium tests.">
      <exec dir="${ws.tests}/testsuite" executable="${ws.lein2}">
         <arg value="with-browser"/>
         <arg value="test"/>
      </exec>
   </target>


DIGITAL JUICE

No comments:

Post a Comment

Thank's!