seeking advice for reducing boilerplate

68 views
Skip to first unread message

Andrew

unread,
Sep 28, 2011, 5:32:55 PM9/28/11
to clo...@googlegroups.com
While trying out clj-webdriver (for testing web pages), I got the impulse to reduce some of my boilerplate. I'd like your advice on best practices. Here's the original code.

(deftest test-login
  (let [b (start :firefox "https://github.com")]
    (try
      (implicit-wait b 60000)
      (-> b (find-it {:text "Login"}) click)
      (-> b (find-it {:class "text", :name "login"}) (input-text "username"))
      (-> b (find-it {:class "text", :name "password"}) (input-text "password"))
      (-> b (find-it :input {:value "Log in"}) click)
      (is (-> b (find-it {:href "/logout"}) present?))
      (finally (-> b quit)))))

In most of my tests, there's going to be (highlighted in red text above) the declaration of the browser, a tweak to how long to wait before declaring something not found, and a try-finally for closing the browser regardless of the test's outcome. I'd like a test definition to just list the browser type (e.g. :firefox) and the URL and the test steps... leaving out the let, the implicit-wait, and the try-finally. Can there be a macro that defines browser b for the test steps that I feed into it?

For the test steps themselves, the way an html element is found (find-it {:text "Login"}) could be factored out and made re-usable...

(defmacro login-link [browser & actions]
  `(-> ~browser (find-it {:text "Login"}) ~@actions))
(defmacro username [browser & actions]
  `(-> ~browser (find-it {:class "text", :name "login"}) ~@actions))
(defmacro password [browser & actions]
  `(-> ~browser (find-it {:class "text", :name "password"}) ~@actions))
(defmacro login-btn [browser & actions]
  `(-> ~browser (find-it :input {:value "Log in"}) ~@actions))

This allows my tests to look like this instead:

(deftest test-login
  (let [b (start :firefox "https://github.com")]
    (try
      (implicit-wait b 60000)
      (login-link b click)
      (username b (input-text "username"))
      (password b (input-text "password"))
      (login-btn b click)
      (is (-> b (find-it {:href "/logout"}) present?))
      (finally (-> b quit)))))

Is this a good way to do it? Can my macros be changed to eliminate the mention of the browser b as the first argument? (I'm guessing this is bad practice) And is there something I can do to reduce the boilerplate in my macro definitions so that I can say this instead...

(defelem login-link {:text "Login"})
(defelem username {:class "text", :name "login"})
(defelem password {:class "text", :name "password"})
(defelem login-btn :input {:value "Log in"})



Kevin Downey

unread,
Sep 28, 2011, 6:57:19 PM9/28/11
to clo...@googlegroups.com
the browser bit should really use with-open

(with-open [browser (create-browser :firefox)]

> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@googlegroups.com
> Note that posts from new members are moderated - please be patient with your
> first post.
> To unsubscribe from this group, send email to
> clojure+u...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en

--
And what is good, Phaedrus,
And what is not good—
Need we ask anyone to tell us these things?

Eric Lavigne

unread,
Sep 28, 2011, 8:47:40 PM9/28/11
to clo...@googlegroups.com
> While trying out clj-webdriver (for testing web pages), I got the impulse to
> reduce some of my boilerplate. I'd like your advice on best practices.

I would start with the main test macro, web-test. I would replace your
local variable b with a dynamically bound var *browser* that web-test
is responsible for setting. The options is a map that allows you to
specify browser and implicit-wait timeout like {:browser :firefox
:timeout 60000} but has reasonable defaults so that you can usually
use {} instead.

(defvar ^:dynamic *browser* nil)

(defmacro web-test [name options & actions]
...)

(web-test test-login {}
(-> *browser* (find-it {:text "Login"})
click)
(-> *browser* (find-it {:class "text", :name "login"})
(input-text "username"))
(-> *browser* (find-it {:class "text", :name "password"})
(input-text "password"))
(-> *browser* (find-it :input {:value "Log in"})
click)
(is (-> *browser* (find-it {:href "/logout"})
present?)))

Next, I'd factor our the "(-> *browser* (find-it" pattern with another macro.

(defmacro web-action [search-parameters & actions]
...)

This converts

(-> *browser* (find-it {:text "Login"})
click)

into

(web-action {:text "Login"} click)

If there is a lot of clicking and entering text in fields, I might
also create click-it and write-it macros so that you can say:

(click-it {:text "Login"})

(write-it {:class "text", :name "login"} "username")

After all these changes, the original example becomes:

(web-test test-login {}
(click-it {:text "Login"})
(write-it {:class "text", :name "login"} "username")
(write-it {:class "text", :name "password"} "password")
(click-it {:value "Log in"})
(is (-> *browser* (find-it {:href "/logout"})
present?)))

Hmm, maybe needs an is-present macro as well?

(is-present {:href "/logout"})

If you have a lot of tests related to the same form, you can also make
shortcuts for commonly references elements:

(def login-text {:class "text", :name "login"})

After all these changes, tests like these should be very easy to write.

Andrew

unread,
Sep 30, 2011, 3:13:23 PM9/30/11
to clo...@googlegroups.com
Thanks (both) for the advice!
Reply all
Reply to author
Forward
0 new messages