This library provides utilities for testing re-frame applications.
These utilities:
- allow you to use
subscribe
in your tests - allow you to use
dispatch
in your tests - allow you to run tests on both the JVM and JS platforms
- allow you to create "end to end" integration tests, involving backend servers
For context, please be sure to read the basic testing tutorial in the main re-frame docs before going any further.
This library primarily supports the testing of Event Handlers, but Subscription Handlers get to come along for the ride.
re-frame-test
provides two macros which dovetail with cljs.test
.
Execute body
as a test, where each dispatch
call is executed
synchronously (via dispatch-sync
), and any subsequent dispatches which are
caused by that dispatch are also fully handled/executed prior to control flow
returning to your test.
Think of it as though every dispatch
in your app had been magically
turned into dispatch-sync
, and re-frame had lifted the restriction that says
you can't call dispatch-sync
from within an event handler.
This macro is applicable for most events that do not run async behaviour within the event.
From the todomvc example:
(defn test-fixtures
[]
;; change this coeffect to make tests start with nothing
(rf/reg-cofx
:local-store-todos
(fn [cofx _]
"Read in todos from localstore, and process into a map we can merge into app-db."
(assoc cofx :local-store-todos
(sorted-map)))))
Define some test-fixtures. In this case we have to ignore the localstore in the tests.
(deftest basic--sync
(rf-test/run-test-sync
(test-fixtures)
(rf/dispatch [:initialise-db])
Use the run-test-sync
macro to construct the tests and initialise the app state.
Note that, the dispatch
will be handled before the following code is executed,
effectively turning it into a dispatch-sync
. Also any changes to the database
and registrations will be rolled back at the termination of the test, therefore
our fixtures are run within the run-test-sync
macro.
;; Define subscriptions to the app state
(let [showing (rf/subscribe [:showing])
sorted-todos (rf/subscribe [:sorted-todos])
todos (rf/subscribe [:todos])
visible-todos (rf/subscribe [:visible-todos])
all-complete? (rf/subscribe [:all-complete?])
completed-count (rf/subscribe [:completed-count])
footer-counts (rf/subscribe [:footer-counts])]
;;Assert the initial state
(is (= :all @showing))
(is (empty? @sorted-todos))
(is (empty? @todos))
(is (empty? @visible-todos))
(is (= false (boolean @all-complete?)))
(is (= 0 @completed-count))
(is (= [0 0] @footer-counts))
;;Dispatch the event that you want to test, remember that `re-frame-test`
;;will process this event immediately.
(rf/dispatch [:add-todo "write first test"])
;;Test that the dispatch has mutated the state in the way that we expect.
(is (= 1 (count @todos) (count @visible-todos) (count @sorted-todos)))
(is (= 0 @completed-count))
(is (= [1 0] @footer-counts))
(is (= {:id 1, :title "write first test", :done false}
(first @todos)))
This macro is applicable for events that do run some async behaviour (usually outside or re-frame, e.g. an http request) within the event.
Run body
as an async re-frame test. The async nature means you'll need to
use wait-for
any time you want to make any assertions that should be true
after an event has been handled. It's assumed that there will be at least
one wait-for
in the body of your test (otherwise you don't need this macro
at all).
Note: unlike regular ClojureScript cljs.test/async
tests, wait-for
takes
care of calling (done)
for you: you don't need to do anything specific to
handle the fact that your test is asynchronous, other than make sure that all
your assertions happen with wait-for
blocks.
From the todomvc example:
(deftest basic--async
(rf-test/run-test-async
(test-fixtures)
(rf/dispatch-sync [:initialise-db])
Use the run-test-async
macro to construct the tests and initialise the app state
note that the dispatch-sync
must be used as this macro does not run the dispatch
immediately like run-test-sync
. Also any changes to the database
and registrations will be rolled back at the termination of the test, therefore
our fixtures are run within the run-test-async
macro.
;;Define subscriptions to the app state
(let [showing (rf/subscribe [:showing])
sorted-todos (rf/subscribe [:sorted-todos])
todos (rf/subscribe [:todos])
visible-todos (rf/subscribe [:visible-todos])
all-complete? (rf/subscribe [:all-complete?])
completed-count (rf/subscribe [:completed-count])
footer-counts (rf/subscribe [:footer-counts])]
;;Assert the initial state
(is (= :all @showing))
(is (empty? @sorted-todos))
(is (empty? @todos))
(is (empty? @visible-todos))
(is (= 0 @completed-count))
;;Dispatch the event that you want to test, remember
;;that re-frame will not process this event immediately,
;;and need to use the `wait-for` macro to continue the tests.
(rf/dispatch [:add-todo "write first test"])
;;Wait for the `:add-todo` event to be dispatched
;;(note, in the use of this macro we would typically wait for
;;an event that had been triggered by the successful return of
;;the async event).
(rf-test/wait-for [:add-todo-finished]
;;Test that the dispatch has mutated the state in the way
;;that we expect.
(is (= [{:id 1, :title "write first test", :done false}] @todos))
Here we have assumed that the :add-todo
event will make some sort of async
call which will in turn generate an add-todo-finished
event when it has finished.
This is not actually the case in the example code.
You will need npm
, with:
$ npm install -g karma karma-cli karma-cljs-test karma-junit-reporter karma-chrome-launcher
And you will need Chrome.
Copyright (c) 2016 Mike Thompson
Distributed under the The MIT License (MIT).