The primary purpose of cuic
is to provide a minimalistic interface for
writing UI tests and leveraging idiomatic clojure.test
convetions as much
as possible. The configuration is done using dynamic variables which pair
nicely with use-fixtures
. All (or at least majority of) project specific
configurations are supposed to be put inside a fixture using binding
blocks
and shared across the test namespaces using this fixture. For some cases
these configurations may be overrided per invocation if necessary.
(ns my.project.common)
(defn ui-test-fixture [t]
(binding [...] ;; configs here
(t)))
(ns my.project.foo-tests
(:require [clojure.test :refer :all]
[cuic.core :as c]
[my.project.common :refer [ui-test-fixture]]))
(use-fixtures :each ui-test-fixture)
(deftest my-foo-test
(c/goto (get-my-server-url))
(c/click (c/find "#my-btn"))
...)
The most important configuration variables are cuic.core/*browser*
,
cuic.core/*timeout*
and cuic.core/*base-url*
. There are also some
other configuration variables, and their usage can be found from
the API docs.
cuic.core/*browser*
This configuration variable defines the default browser that will be used
in cuic.core
queries: they can't be used without setting this value (unless
browser is explictly defined per invocation). The value must be a valid
Chrome instance that can be obtained from cuic.chrome/launch
. Chrome instances
implement java.lang.AutoCloseable
, meaning that they are compatibile with
Clojure's with-open
macro. The most usual pattern is to combine with-open
and binding
blocks in your fixture:
(ns my.project.common
(:require [cuic.core :as c]
[cuic.chrome :as chrome]))
(defn ui-test-fixture [t]
(with-open [chrome (chrome/launch)]
(binding [c/*browser* chrome]
(t))))
cuic.core/*timeout*
This configuration variable controls the timeout value (in milliseconds) that queries and actions use when they wait for dom to be ready to satisfy their requirements (e.g. element appears in dom or becomes visible and enabled for clicking). Generally you want to configure this variable to be as small as you can in your local development environment but because CI machines tend to be slower, increasing the value to make tests more robust against network / CPU hiccups and asynchronous UI interactions (such as animations).
(ns my.project.common
(:require [cuic.core :as c]
[cuic.chrome :as chrome]))
(defn ui-test-fixture [t]
(binding [c/*timeout* (if (is-local-env?) 2000 20000)]
(t)))
cuic.core/*base-url*
This configuration variable is optional but very useful. It defines the
base url that will be used in cuic.core/goto
if no host is given. This
allows you to decouple your test case from the used server instance: it can
be for example spinned locally to a random port per each test case, or it
can use QA environment or any other host accessible from the test machine.
(ns my.project.common
(:require [cuic.core :as c]
[cuic.chrome :as chrome]))
(defn ui-test-fixture [t]
(binding [c/*base-url* "http://qa.my-app.com"]
(t)))
...
(deftest login-test
(c/goto "/login") ;; will use http://qa.my-app.com/login as browser address
...)
This allows you to for example start new server instance per test without the test actually knowning about the server hostname or port.
After setting the browser and timeout (and optionally server's base url),
your ui-test-fixture
might look somehow like this:
(ns my.project.common
(:require [cuic.core :as c]
[cuic.chrome :as chrome]))
(defn ui-test-fixture [t]
(with-open [chrome (chrome/launch)
server (launch-my-app-server)]
(binding [c/*browser* chrome
c/*timeout* (if (is-local-env?) 5000 20000)
c/*base-url* (str "http://localhost:" (:port server))]
(t))))
Propably your test requirements are more complex, and the fixture might need
to set up other things as well (such as server or database) but they are out
of the scope of this article. If your requirements are simple, you might also
be able to use cuic.test/browser-test-fixture
which handle these configurations
automatically for you.
Note also that if you're not running your tests in parallel, you can share
the Chrome instance across tests runs by launching it before the startup of
the test suite and closing it after the suite is finished (consult your test
runner's documentation for finding proper hooks for this). This may significantly
imporove your test suite run time because lauching single Chrome instance
may take up-to few seconds per launch. At least use :once
instead of
:each
for your ui-test-fixture
whenever possible.
It's generally not advisable to run UI tests in parallel because they require a lot of resources and running them in parallal may slow their execution (especially in CI machines), resulting in false positive timeouts and flaky tests.
deftest*
and is*
is*
When testing UIs, practically every step should be considered
asynchronous. This means that every assertion must be waited.
Writing (is (c/wait ...))
everywhere is a tedious task and gets
forgotten easily. In addition, if you're using a custom test runner
that pretty prints failed assertions and their diffs (e.g. eftest
or Cursive's test runner), c/wait
will mess up the pretty printing.
To avoid this, cuic
provides cuic.test/is*
that is a shorthand
for (is (c/wait ...))
with some test reporter magic to preserve
pretty printing. It also tries to capture screenshot from the active
browser page in case of assertion failure. Screenshots will be saved
under target/screenshots
by default, but the location can be changed
by overriding the [[cuic.test/screenshot-options]] configration
varible (:dir
option).
The usage of is*
does not differ from traditional
is
usage at all:
(is (c/wait (c/visible? (save-button))))
(is (c/wait (c/has-class? (save-button) "primary")))
(is (c/wait (re-find #"Changes saved" (c/inner-text (save-summary)))))
;;;; =>
(require '[cuic.test :refer [is*])
(is* (c/visible? (save-button)))
(is* (c/has-class? (save-button) "primary"))
(is* (re-find #"Changes saved" (c/inner-text (save-summary))))
Like wait
, also is*
can be used for non-UI assertions:
(add-todo "Foo")
(add-todo "Bar")
(ogy-click "Save")
(is* (= #{"Foo" "Bar"} (get-todos-from-db))))
deftest*
Because of the nature of UI tests, first assertion failure will usually
indicate the failure of the remaining assertions as well. If each of
these assertions wait the timeout before giving up, the test run might
prolong quite a bit. That's why is*
aborts the current test run
immediately after the first failed assertion (this behaviour can be
changed with [[cuic.test/abort-immediately]] configuration variable).
Aborting is implemented by throwing an AbortTestError
from the failed
assertion. If you're using deftest
, the test suite catches and reports
this error as well. If you want to avoid this unnecessary "double failure",
you can switch the clojure.test/deftest
to cuic.test/deftest*
which
will suppress the error gracefully. It also provides more information to
is*
assertions about the executed test, resulting in more accurate
screenshot filenames.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close