The macros in untangled-spec wrap clojure/cljs test, so that you may use any of the features of the core library. The specification DSL makes it much easier to read the tests, and also includes a number of useful features:
-
Outline rendering
-
Left-to-right assertions
-
More readable output, such as data structure comparisons on failure (with diff notation as well)
-
Real-time refresh of tests on save (client and server)
-
Seeing test results in any number of browsers at once
-
Mocking of normal functions, including native javascript (but as expected: not macros or inline functions)
-
Mocking verifies call sequence and call count
-
Mocks can easily verify arguments received
-
Mocks can simulate timelines for CSP logic
-
Protocol testing support (helps prove network interactions are correct without running the full stack)
A dev-time only entry point for browser test rendering:
(ns cljs.user
(:require
[untangled-spec.tests-to-run] ;;(1)
[untangled-spec.suite :as suite]
[untangled-spec.selectors :as sel]))
(suite/def-test-suite on-load ;;(2)
{:ns-regex #"untangled-spec\..*-spec"} ;;(3)
{:available #{:focused :should-fail} ;;(4)
:default #{::sel/none :focused}}) ;;(5)
-
tests_to_run.cljs Just requires all of your tests. This is necessary as all cljs test runners search by looking at the loaded namespaces. Since there are two places cljs tests can run from (browser and CI), it makes sense to keep this list in one file.
-
Define a callback for figwheel to call to re run the tests.
-
:ns-regex
is the regex to filter the loaded namespaces down to just your tests. -
Is where you define your
:available
selectors, ie: legal/possible values for selectors on yourspecification
. -
:default
selectors to use when you haven’t yet specified any in the browser ui.
A cljsbuild in your project.clj (or if using boot: roughly) as follows:
{:source-paths ["src" "dev" "test"]
:figwheel {:on-jsload cljs.user/on-load} ;;(1)
:compiler {:main cljs.user ;;(2)
:output-to "resources/public/js/test/test.js" ;;(3)
:output-dir "resources/public/js/test/out"
:asset-path "js/test/out"
:optimizations :none}}
-
References the earlier defined test suite name so figwheel can re-run the tests when it reloads the js.
-
Namespace entrypoint (eg:
:main
) points to the dev user namespace as described above. -
Must compile to a single javascript file at
"resources/public/js/test/test.js"
.
An entrypoint for starting figwheel (or something equivalent if in boot):
(:require
[com.stuartsierra.component :as cp]
[figwheel-sidecar.system :as fsys])
(defn start-figwheel
"Start Figwheel on the given builds, or defaults to build-ids in `figwheel-config`."
([]
(let [props (System/getProperties) ;;(1)
figwheel-config (fsys/fetch-config)
all-builds (->> figwheel-config :data :all-builds (mapv :id))]
(start-figwheel (keys (select-keys props all-builds)))))
([build-ids]
(let [figwheel-config (fsys/fetch-config)
target-config (-> figwheel-config
(assoc-in [:data :build-ids]
(or (seq build-ids) ;;(2)
(-> figwheel-config :data :build-ids))))] ;;(2)
(-> (cp/system-map
:css-watcher (fsys/css-watcher {:watch-paths ["resources/public/css"]}) ;;(3)
:figwheel-system (fsys/figwheel-system target-config))
cp/start :figwheel-system fsys/cljs-repl)))) ;;(4)
-
If no arguments, default to looking at the JVM system properties,
eg: if you have a build with id:test
, it will look for-Dtest
-
Otherwise it will use the passed in
build-ids
, or the default figwheel ids. -
OPTIONAL, starts a css watcher so you get automatic reloading of css when it changes.
-
Starts a cljs repl you can interact with, see figwheel sidecar for more information
Open localhost:PORT/untangled-spec-client-tests.html
, where PORT
is defined (with leiningen) by your figwheel configuration in your project.clj, eg: :figwheel {:server-port PORT}
.
-
You should have a top level package.json file for installing the following:
{
"devDependencies": {
"karma": "^0.13.19",
"karma-chrome-launcher": "^0.2.2",
"karma-firefox-launcher": "^0.1.7",
"karma-cljs-test": "^0.1.0"
}
}
-
You have lein-doo as a plugin for running tests through karma via nodejs.
-
A all_tests.cljs file for running your tests.
(ns your.all-tests
(:require
untangled-spec.tests-to-run ;; (1)
[doo.runner :refer-macros [doo-all-tests]]))
(doo-all-tests #"untangled-spec\..*-spec") ;; (2)
-
For loading your test namespaces.
-
Takes a regex to run just your test namespaces.
-
A
:doo
section to configure the CI runner. Chrome is the recommended target js-env.
:doo {:build "automated-tests", :paths {:karma "node_modules/karma/bin/karma"}}
-
A cljsbuild with id
:automated-tests
is the CI tests output.
{:source-paths ["src" "test"]
:compiler {:output-to "resources/private/js/unit-tests.js"
:main untangled-spec.all-tests
:optimizations :none}}
-
An html file in your
resources/private/
, eg: unit-tests.html, for renderering yourautomated-tests
build.
See lein-doo usage for up to date details on how to use it from the command line and how to setup the all_tests like file,
TLDR: lein doo ${js-env} automated-tests once
, where for ex ${js-env}
is chrome
.
-
The lein test-refresh plugin, which will re-run server tests on save, and can be configured (see the
:test-refresh
section in the project.clj). -
user.clj : The entry point for running clojure tests that should be rendered in the browser.
Use lein test-refresh
at the command line.
Read this changelog entry
for information on using test-selectors.
A recommended sample configuration is:
:test-refresh {:report untangled-spec.reporters.terminal/untangled-report (1)
:changes-only true (2)
:with-repl true} (3)
-
REQUIRED,
:report
must point to the correct report function. -
Only re-runs tests that have changed, useful if you have slow and/or many tests.
-
Gives you a limited, but handy, repl!
For up to date and comprehensive information, you should treat lein-test-refresh itself as the authoritative source.
-
Create a webserver using
untangled-spec.suite/def-test-suite
that runs your tests and talks with your browser.
See the docstring for further info on the arguments.
(:require
[untangled-spec.suite :as suite])
(suite/def-test-suite my-test-suite (1)
{:config {:port 8888} (2)
:test-paths ["test"] (3)(4)
:source-paths ["src"]} (4)
{:available #{:focused :unit :integration} (5)
:default #{:untangled-spec.selectors/none :focused :unit}}) (6)
-
Defines a function you can call to (re-)start the test-suite, should be useful if you are changing the following arguments, ie: config, paths, or selectors.
-
:config
is passed directly to the webserver, only port is required and currently advertised as available. -
:test-paths
is about finding your test namespaces. -
:source-paths
is concatenated with:test-paths
to create a set of paths that the test-suite will watch for any changes, and refresh and namespaces contain therein. -
Is where you define your
:available
selectors, ie: legal/possible values for selectors on yourspecification
. -
:default
selectors to use when you haven’t yet specified any in the browser ui.
-
Call
my-test-suite
and go tolocalhost:PORT/untangled-spec-server-tests.html
to view your test report.
The main testing macros are specification
, behavior
, component
, and assertions
:
(:require
[untangled-spec.core :refer [specification behavior component assertions])
(specification "A Thing"
(component "A Thing Part"
(behavior "does something"
(assertions
form => expected-result
form2 => expected-result2
"optional sub behavior clause"
form3 => expected-result3)))
See the clojure.spec/def for ::assertions
in assertions.cljc for the grammar of the assertions
macro.
ℹ️
|
💡
|
You are therefore free to use any functions from clojure.test or cljs.test inside their body. However, we recommend you use these macros as opposed to |
Assertions provides some explict arrows, unlike Midje which uses black magic, for use in making your tests more concise and readable.
(:require
[untangled-spec.core :refer [assertions])
(assertions
actual => expected ;;(1)
actual =fn=> (fn [act] ... ok?) ;;(2)
actual =throws=> ExceptionType ;; (3)(6)
actual =throws=> (ExceptionType opt-regex opt-pred) ;;(4)(6)
actual =throws=> {:ex-type opt-ex-type :regex opt-regex :fn opt-pred}) ;; (5)(6)
-
Checks that actual is equal to expected, either can be anything.
-
expected
is a function takesactual
and returns a truthy value. -
Expects that actual will throw an Exception and checks that the type is
ExceptionType
. -
Can also optionally that the message matches the
opt-regex
&opt-pred
. -
An alternative supported syntax is a map with all optional keys
:ex-type
:regex
:fn
-
View the clojure.spec/def
::criteria
assertions.cljc for the up to date grammar for theexpected
side of a=throws⇒
assertions.
The mocking system does a lot in a very small space. It can be invoked via the provided
or when-mocking
macro.
The former requires a string and adds an outline section. The latter does not change the outline output.
The idea with provided
is that you are stating an assumption about some way other parts of the system are behaving for that test.
Mocking must be done in the context of a specification, and creates a scope for all sub-outlines. Generally you want to isolate mocking to a specific behavior:
(:require
[untangled-spec.core :refer [specification behavior when-mocking assertions])
;; source file
(defn my-function [x y] (launch-rockets!))
;; spec file
(specification "Thing"
(behavior "Does something"
(when-mocking
(my-function arg1 arg2)
=> (do (assertions
arg1 => 3
arg2 => 5)
true)
;;actual test
(assertions
(my-function 3 5) => true))))
Basically, you include triples (a form, arrow, form), followed by the code & tests to execute.
It is important to note that the mocking support does a bunch of verification at the end of your test:
-
It uses the mocked functions in the order specified.
-
It verifies that your functions are called the appropriate number of times (at least once is the default) and no more if a number is specified.
-
It captures the arguments in the symbols you provide (in this case arg1 and arg2). These are available for use in the RHS of the mock expression.
-
If the mocked function has a
clojure.spec/fdef
with:args
, it will validate the arguments with it. -
It returns whatever the RHS of the mock expression indicates.
-
If the mocked function has a
clojure.spec/fdef
with:ret
, it will validate the return value with it. -
If the mocked function has a
clojure.spec/fdef
with:fn
(and:args
&:ret
), it will validate the arguments and return value with it. -
If assertions run in the RHS form, they will be honored (for test failures).
So, the following mock script should pass:
(:require
[untangled-spec.core :refer [when-mocking assertions])
(when-mocking
(f a) =1x=> a ;;(1)
(f a) =2x=> (+ 1 a) ;;(2)
(g a b) => 17 ;;(3)
(assertions
(+ (f 2) (f 2) (f 2)
(g 3e6 :foo/bar)
(g "otherwise" :invalid)) (4)
=> 42))
-
The first call to
f
returns the argument. -
The next two calls return the argument plus one.
-
g
can be called any amount (but at least once) and returns 17 each time. -
If you were to remove any call to
f
org
this test would fail.
However, the following mock script will fail due to clojure.spec errors:
(:require
[clojure.spec :as s]
[untangled-spec.core :refer [when-mocking assertions])
(s/fdef f
:args number?
:ret number?
:fn #(< (:args %) (:ret %)))
(defn f [a] (+ a 42))
(when-mocking
(f "asdf") =1x=> 123 ;; (1)
(f a) =1x=> :fdsa ;; (2)
(f a) =1x=> (- 1 a) ;; (3)
(assertions
(+ (f "asdf") (f 1) (f 2)) => 42))
-
Fails the
:args
specnumber?
-
Fails the
:ret
specnumber?
-
Fails the
:fn
spec(< args ret)
Sometimes it is desirable to check that a function is called but still use its original definition, this pattern is called a test spy. Here’s an example of how to do that with untangled spec:
(:require
[untangled-spec.core :refer [when-mocking assertions])
(let [real-fn f]
(when-mocking f => (do ... (real-fn))
(assertions
...)
When working with protocols and records, or inline functions (eg: +), it is useful to be able to mock them just as a regular function. The fix for doing so is quite straightforward:
;; source file
(defprotocol MockMe
(-please [this f x] ...)) ;;(1)
(defn please [this f x] (-please this f x)) ;;(2)
(defn fn-under-test [this]
... (please this inc :counter) ...) ;;(3)
;; test file
(:require
[untangled-spec.core :refer [when-mocking assertions])
(when-mocking
(please this f x) => (do ...) ;;(4)
(assertions
(fn-under-test ...) => ...))) ;;(5)
-
define the protocol & method
-
define a function that just calls the protocol
-
use the wrapper function instead of the protocol
-
mock the wrapping function from (2)
-
keep calm and carry on testing
On occasion you’d like to mock things that use callbacks. Chains of callbacks can be a challenge to test, especially when you’re trying to simulate timing issues.
(:require
[cljs.test :refer [is]]
[untangled-spec.core :refer [specification provided with-timeline
tick async]])
(def a (atom 0))
(specification "Some Thing"
(with-timeline
(provided "things happen in order"
(js/setTimeout f tm) =2x=> (async tm (f))
(js/setTimeout
(fn []
(reset! a 1)
(js/setTimeout
(fn [] (reset! a 2)) 200)) 100)
(tick 100)
(is (= 1 @a))
(tick 100)
(is (= 1 @a))
(tick 100)
(is (= 2 @a))))
In the above scripted test the provided
(when-mocking with a label) is used to mock out js/setTimeout
. By
wrapping that provided in a with-timeline
we gain the ability to use the async
and tick
macros (which must be
pulled in as macros in the namespace). The former can be used on the RHS of a mock to indicate that the actual
behavior should happen some number of milliseconds in the simulated future.
So, this test says that when setTimeout
is called we should simulate waiting however long that
call requested, then we should run the captured function. Note that the async
macro doesn’t take a symbol to
run, it instead wants you to supply a full form to run (so you can add in arguments, etc).
Next this test does a nested setTimeout
! This is perfectly fine. Calling the tick
function advances the
simulated clock. So, you can see we can watch the atom change over \"time\"!
Note that you can schedule multiple things, and still return a value from the mock!
(:require
[untangled-spec.core :refer [provided with-timeline async]])
(with-timeline
(when-mocking
(f a) => (do (async 200 (g)) (async 300 (h)) true)))
the above indicates that when f
is called it will schedule (g)
to run 200ms from \"now\" and (h)
to run
300ms from \"now\". Then f
will return true
.