Clojure is a dialect of Lisp that runs on the JVM. It has very elegant syntax and a novel approach to concurrency.
In this installment, we’ll build a Clojure framework for running Selenium tests in parallel. We’ll use Clojure’s Java interop to interact with Selenium, and agents to distribute the work. Once you have the tests executing in parallel, you can run them against your internal selenium farm or use our Sauce OnDemand cloud hosted service to get running with no further configuration.
First, the framework, in about 50 lines of code:
(import '[com.thoughtworks.selenium DefaultSelenium])
(defn- run-with-client
"Run a test function with new client"
[test-fn opts]
(let [client (new DefaultSelenium (:host opts) (:port opts) (:command opts)
(:url opts))]
(.start client)
(try
(test-fn client)
(catch Exception e false)
(finally
(.stop client)))))
(defn- run-single-test
"Run a single test, append test result to results"
[test results opts]
(let [test-fn (test :test)
value (run-with-client test-fn opts)
res {:test (test :name) :value value}]
(dosync (alter results (fn [old] (conj old res))))))
(defn- gen-agents
[num-agents]
(take num-agents (map agent (repeat nil))))
(defn- parse-options
"Get options as hash map, provide defaults"
[args]
(let [opts (apply hash-map args)] ; Convert [:port 4444] to {:port 4444}
(-> opts
(assoc :port (:port opts 4444))
(assoc :host (:host opts "localhost"))
(assoc :command (:command opts "*firefox"))
(assoc :url (:url opts "http://localhost/"))
(assoc :num-agents (:num-agents opts 4)))))
(defn run-tests
"Run tests in parallel, return list of test results.
After tests are done, passes the list of results to reporter.
A test has the format
{
:name NAME
:test (fn [client] ...)
}
"
[tests reporter & args]
(let [opts (parse-options args)
agents (gen-agents (:num-agents opts))
results (ref [])]
(doseq [[test agent] (map vector tests (cycle agents))]
(send agent (fn [_] (run-single-test test results opts))))
(doseq [agent agents] (await agent)) ; Wait for tests to finish
(shutdown-agents)
(reporter @results)
@results))
(defn tests-failed?
"Check if all tests passed"
[results]
(not (empty? (filter false? (map :value results)))))
Now let’s define some tests and run them:
(require 'selenium)
(def test-google
{
:name "google"
:test (fn [client]
(doto client
(.open "http://www.google.com")
(.type "q" "Sauce Labs")
(.click "btnG")
(.waitForPageToLoad "5000"))
(.isTextPresent client "Selenium"))
})
(def test-yahoo
{
:name "yahoo"
:test (fn [client]
(doto client
(.open "http://yahoo.com")
(.type "p" "Sauce Labs")
(.click "search-submit")
(.waitForPageToLoad "5000"))
(.isTextPresent client "Selenium"))
})
(def test-bing
{
:name "bing"
:test (fn [client]
(doto client
(.open "http://www.bing.com")
(.type "q" "Sauce Labs")
(.click "go")
(.waitForPageToLoad "5000"))
(.isTextPresent client "Selenium"))
})
(def tests [test-google test-yahoo test-bing])
; Our fancy reporter :)
(defn reporter
[results]
(doseq [r results]
(println r)))
(let [results (run-tests tests reporter)]
(if (tests-failed? results)
(System/exit 1)))
To run the system locally, you’ll need the Selenium server and client jars and the Clojure jar (we’re using 1.1.0). You can use Sauce RC to get a Selenium server up and running using a GUI-based installer. Then, just issue the following command:
java -jar selenium-server.jar&
java -cp selenium-java-client-driver.jar:clojure.jar \
clojure.main test-sauce.clj
(See the run-tests script, you can control the number of concurrent tests by providing :num-agents N to run-tests)
To run it against our cloud hosted service without having to configure any servers, simply follow the instructions at Sauce OnDemand documentation and supply the right options to run-tests. It will probably look something like:
(run-tests tests reporter
:host "saucelabs.com"
:command "{
\"username\" : \"SAUCE-USER-NAME\",
\"access-key\" : \"SAUCE-API-KEY\",
\"os\" : \"Windows 2003\",
\"browser\" : \"firefox\",
\"browser-version\" : \"3.5.\"}"
:url "http://saucelabs.com")
You won’t need to run any servers locally, just execute the clojure test:
java -cp selenium-java-client-driver.jar:clojure.jar \
clojure.main test-sauce.clj
Future enhancements to this framework might include better reporting, and integration with the clojure.test testing framework.
All of the code from this installment, including the needed jar files can be found at http://github.com/saucelabs/parallel-test-examples/tree/master/clojure.
