re-frame/docs/Testing.md

327 lines
11 KiB
Markdown
Raw Normal View History

2016-10-19 10:24:19 +00:00
## Testing
2017-07-14 04:24:11 +00:00
This is an introduction to testing re-frame apps. It
walks you through some choices.
2017-06-02 05:25:14 +00:00
## What To Test
For any re-frame app, there's three things to test:
2017-07-14 04:24:11 +00:00
- **Event Handlers** - most of your testing focus will
be here because this is where most of the logic lives
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
- **Subscription Handlers** - often not a lot to test here. Only
[Layer 2](SubscriptionInfographic.md) subscriptions need testing.
2017-06-02 05:25:14 +00:00
- **View functions** - I don't tend to write tests for views. There, I said it.
2017-07-14 04:24:11 +00:00
Hey! It is mean to look at someone with that level of disapproval,
while shaking your head. I have my reasons ...<br>
In my experience with the re-frame architecture, View Functions
2017-06-02 05:25:14 +00:00
tend to be an unlikely source of bugs. And every line of code you write is
2017-07-14 04:24:11 +00:00
like a ball & chain you must forevermore drag about, so I dislike maintaining
2017-06-02 05:25:14 +00:00
tests which don't deliver good bang for buck.
2017-07-14 04:43:51 +00:00
2017-07-14 04:24:11 +00:00
And, yes, in theory there's also `Effect Handlers` (Domino 3) to test,
but you'll hardly ever write one, and, anyway, each one is different, so
I've got no good general insight to offer you for them. They will be ignored
in this tutorial.
2017-06-02 05:25:14 +00:00
## Test Terminology
2017-07-14 04:24:11 +00:00
Let's establish some terminology to aid the further explanations in this
tutorial. Every unittest has 3 steps:
2017-06-02 05:25:14 +00:00
1. **setup** initial conditions
2. **execute** the thing-under-test
3. **verify** that the thing-under-test did the right thing
2017-07-14 04:24:11 +00:00
## Exposing Event Handlers For Test
2017-06-02 05:25:14 +00:00
2017-07-14 04:43:51 +00:00
Event Handlers are pure functions which should make them easy to test, right?
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
First, create a named event handler using `defn` like this:
2016-10-19 10:24:19 +00:00
```clj
2017-06-02 05:25:14 +00:00
(defn select-triangle
[db [_ triangle-id]
2016-10-19 10:24:19 +00:00
... return a modified version of db)
```
2017-07-14 04:24:11 +00:00
You'd register this handler in a separate step:
2016-10-19 10:24:19 +00:00
```clj
2017-07-14 04:24:11 +00:00
(re-frame.core/reg-event-db ;; this is a "-db" event handler, not "-fx"
2017-06-02 05:25:14 +00:00
:select-triangle
2016-10-19 10:24:19 +00:00
[some-interceptors]
2017-06-02 05:25:14 +00:00
select-triangle) ;; <--- defn above. don't use an annonomous fn
```
2017-07-14 04:24:11 +00:00
This arrangement means the event handler function
2017-06-02 05:25:14 +00:00
`select-triangle` is readily available to be unittested.
## Event Handlers - Setup - Part 1
To test `select-triangle`, a unittest must pass in values for the two arguments
2017-07-14 04:24:11 +00:00
`db` and `v`. And, so, our **setup** would have to construct both values.
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
But how to create a useful `db` value?
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
`db` is a map of a certain structure, so one way would be to simply `assoc` values
into a map at certain paths to simulate a real-world `db` value or, even easier, just use
2017-06-02 05:25:14 +00:00
a map literal, like this:
```cljs
;; a test
(let [
2017-07-14 04:24:11 +00:00
;; setup - create db and event
2017-06-02 05:25:14 +00:00
db {:some 42 :thing "hello"} ; a literal
2017-07-14 04:24:11 +00:00
event [:select-triange :other :event :args]
2017-06-02 05:25:14 +00:00
;; execute
2017-07-14 04:24:11 +00:00
result-db (select-triange db event)]
;; validate that result-db is correct)
(is ...)
2017-06-02 05:25:14 +00:00
```
2017-07-14 04:24:11 +00:00
This certainly works in theory, but in practice,
unless we are careful, constructing the `db`
value in **setup** could:
2017-06-02 05:25:14 +00:00
* be manual and time consuming
* tie tests to the internal structure of `app-db`
2017-07-14 04:24:11 +00:00
The **setup** of every test could end up relying on the internal structure
2017-06-02 05:25:14 +00:00
of `app-db` and any change in that structure (which is inevitable over time)
2017-07-14 04:24:11 +00:00
would result in a lot re-work in the tests. That's too fragile.
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
So, this approach doesn't quite work.
2017-06-02 05:25:14 +00:00
## Event Handlers - Setup - Part 2
2017-07-14 04:24:11 +00:00
> In re-frame, `Events` are central. They are the "language of the system". They
provide the eloquence.
The `db` value (stored in `app-db`) is the cumulative result
of many event handlers running.
We can use this idea. In **setup**, instead of manually trying to create that `db` value, we could
"build up" a `db` value by threading `db` through many event handlers
which cumulatively create the required initial state. Tests then need
know nothing about the internal structure of that `db`.
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
Like this:
2017-06-02 05:25:14 +00:00
```clj
(let [
2017-07-14 04:24:11 +00:00
;; setup - cummulatively build up db
2017-06-02 05:25:14 +00:00
db (-> {} ;; empty db
2017-07-14 04:24:11 +00:00
(initialise-db [:initialise-db]) ;; each event handler expects db and event
2017-06-02 05:25:14 +00:00
(clear-panel [:clear-panel])
(draw-triangle [:draw-triangle 1 2 3]))
2017-07-14 04:24:11 +00:00
2017-06-02 05:25:14 +00:00
event [:select-triange :other :stuff]
2017-07-14 04:24:11 +00:00
;; now execute the event handler under test
2017-06-02 05:25:14 +00:00
db' (select-triange db event)]
2017-07-14 04:24:11 +00:00
;; validate that db' is correct
(is ...)
2016-10-19 10:24:19 +00:00
```
2017-06-02 05:25:14 +00:00
This approach works so long as all the event handlers are
2017-07-14 04:24:11 +00:00
of the `-db` kind, but the threading gets a little messy when some event
handlers are of the `-fx` kind which take a `coeffect` argument and
return `effects`, instead of a `db` value.
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
So, this approach is quite workable in some cases, but can get messy
in the general case.
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
## Event Handlers - Setup - Part 3
2016-10-19 10:24:19 +00:00
2017-07-14 04:24:11 +00:00
There is further variation which is quite general but not as pure.
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
During test **setup** we could literally just `dispatch` the events
which would put `app-db` into the right state.
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
Except, we'd have to use `dispatch-sysnc` rather `dispatch` to
force immediate handling of events, rather than queuing.
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
```clj
;; setup - cummulatively build up db
(dispatch-sync [:initialise-db])
(dispatch-sync [:clear-panel])
(dispatch-sync [:draw-triangle 1 2 3]))
;; execute
(dispatch-sync [:select-triange :other :stuff])
;; validate that the valuein `app-db` is correct
;; perhaps with subscriptions
```
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
Notes:
1. we use `dispatch-sync` because `dispatch` is async (event is handled not now, but soon)
2. Not pure. We are choosing to mutate the global `app-db`. But
having said that, there's something about this approach with is remarkably
pragmatic.
2. the **setup** is now very natural. The associated handlers can be either `-db` or `-fx`
3. if the handlers have effects other than just updating app-db, we might need to stub out XXX
4. How do we look at the results ????
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
If this method appeals to you, you should ABSOLUTELY review the utilities in this helper library:
2017-06-02 05:25:14 +00:00
[re-frame-test](https://github.com/Day8/re-frame-test).
2016-10-19 10:24:19 +00:00
2017-07-14 04:43:51 +00:00
In summary, event handlers should be easy to test because they are pure functions. The interesting
part is the unittest "setup" where we need to establishing an initial value for `db`.
2016-10-19 10:24:19 +00:00
## Subscription Handlers
2017-07-14 04:24:11 +00:00
Here's a Subscription Handler from
[the todomvc example](https://github.com/Day8/re-frame/blob/master/examples/todomvc/src/todomvc/subs.cljs):
2016-10-19 10:24:19 +00:00
```clj
(reg-sub
2017-05-29 14:54:18 +00:00
:visible-todos
;; signal function
(fn [query-v _]
[(subscribe [:todos])
(subscribe [:showing])])
;; computation function
(fn [[todos showing] _] ;; that 1st parameter is a 2-vector of values
(let [filter-fn (case showing
:active (complement :done)
:done :done
:all identity)]
(filter filter-fn todos))))
2016-10-19 10:24:19 +00:00
```
How do we test this?
2017-07-14 04:24:11 +00:00
First, we could split the computation function from its registration, like this:
2016-10-19 10:24:19 +00:00
```clj
2017-05-29 14:54:18 +00:00
(defn visible-todos
[[todos showing] _]
2016-10-19 10:24:19 +00:00
2017-05-29 14:54:18 +00:00
(let [filter-fn (case showing
:active (complement :done)
:done :done
:all identity)]
(filter filter-fn todos)))
2016-10-19 10:24:19 +00:00
2017-05-29 14:54:18 +00:00
(reg-sub
:visible-todos
(fn [query-v _]
[(subscribe [:todos])
(subscribe [:showing])])
visible-todos) ;; <--- computation function used here
2016-10-19 10:24:19 +00:00
```
2017-06-02 05:25:14 +00:00
That makes `visible-todos` available for direct unit testing.
2016-10-19 10:24:19 +00:00
2017-05-29 14:54:18 +00:00
## View Functions - Part 1
2016-10-19 10:24:19 +00:00
2017-07-14 04:24:11 +00:00
Components/views are more tricky and there are a few options.
2016-10-19 10:24:19 +00:00
2017-07-14 04:24:11 +00:00
But remember my ugly secret - I don't tend to write tests for my views.
2016-10-19 10:24:19 +00:00
2017-07-14 04:24:11 +00:00
But here's how, theoretically, I'd write tests if I wasn't me ...
2017-06-02 05:25:14 +00:00
2017-07-14 04:24:11 +00:00
If a View Function is a [Form-1](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-1-a-simple-function)
2017-05-29 14:54:18 +00:00
structure, then it is fairly easy to test.
2016-10-19 10:24:19 +00:00
A trivial example:
```clj
(defn greet
[name]
[:div "Hello " name])
(greet "Wiki")
;;=> [:div "Hello " "Wiki"]
```
2017-07-14 04:24:11 +00:00
So, here, testing involves passing values into the function and checking the data structure returned
for correctness.
2016-10-19 10:24:19 +00:00
What's returned is hiccup, of course. So how do you test hiccup for correctness?
2017-05-29 14:54:18 +00:00
hiccup is just a clojure data structure - vectors containing keywords, and maps, and other vectors, etc.
2017-07-14 04:24:11 +00:00
Perhaps you'd use https://github.com/nathanmarz/specter to declaratively check on the presence
of certain values and structures? Or do it more manually.
2016-10-19 10:24:19 +00:00
2017-05-29 14:54:18 +00:00
## View Functions - Part 2A
2016-10-19 10:24:19 +00:00
2017-07-14 04:43:51 +00:00
But what if the View Function has a subscription?
2016-10-19 10:24:19 +00:00
```clj
(defn my-view
[something]
(let [val (subscribe [:query-id])] <-- reactive subscription
2017-05-29 14:54:18 +00:00
[:div .... using @val in here])))
2016-10-19 10:24:19 +00:00
```
2017-07-14 04:43:51 +00:00
The use of `subscribe` makes the function impure (it obtains data from places other than its args).
2016-10-19 10:24:19 +00:00
2017-07-14 04:43:51 +00:00
A testing plan might be:
1. setup `app-db` with some values in the right places (via dispatch of events?)
2017-05-29 14:54:18 +00:00
2. call `my-view` (with a parameter) which will return hiccup
3. check the hiccup structure for correctness.
2016-10-19 10:24:19 +00:00
Continuing on, in a second phase you could then:
5. change the value in `app-db` (which will cause the subscription to fire)
2017-05-29 14:54:18 +00:00
6. call view functions again (hiccup returned).
2016-10-19 10:24:19 +00:00
7. check that the hiccup
2017-07-14 04:43:51 +00:00
Which is all possible, if a little messy.
2016-10-19 10:24:19 +00:00
2017-05-29 14:54:18 +00:00
## View Functions - Part 2B
2016-10-19 10:24:19 +00:00
2017-07-14 04:43:51 +00:00
There is a very pragmatic method available to handle the impurity: use `with-redefs`
to stub out `subscribe`. Like this:
2016-10-19 10:24:19 +00:00
```clj
(defn subscription-stub [x]
(atom
(case x
[:query-id] 42)))
(deftest some-test
(with-redefs [re-frame/subscribe (subscription-stub)]
2017-07-14 04:43:51 +00:00
(testing "some some view which does a subscribe"
..... call the view function and the hiccup output)))
2016-10-19 10:24:19 +00:00
```
2017-05-29 14:54:18 +00:00
For more integration level testing, you can use `with-mounted-component`
2017-07-14 04:24:11 +00:00
from the [reagent-template](https://github.com/reagent-project/reagent-template/blob/master/src/leiningen/new/reagent/test/cljs/reagent/core_test.cljs)
to render the component in the browser and validate the generated DOM.
2016-10-19 10:24:19 +00:00
2017-05-29 14:54:18 +00:00
## View Functions - Part 2C
2016-10-19 10:24:19 +00:00
2017-07-14 04:43:51 +00:00
Or ... there is another option: you can structure in the first place for pure view functions.
2016-10-19 10:24:19 +00:00
The trick here is to create an outer and inner component. The outer sources the data
2017-07-14 04:43:51 +00:00
(via a subscription), and passes it onto the inner as props (parameters).
2016-10-19 10:24:19 +00:00
As a result, the inner component, which does the testable work, is pure and
2017-07-14 04:43:51 +00:00
easily tested. The outer is impure but fairly trivial.
2016-10-19 10:24:19 +00:00
2017-06-02 05:25:14 +00:00
To get a more concrete idea, I'll direct you to another page in the docs
2016-10-19 10:24:19 +00:00
which has nothing to do with testing, but it does use this `simple-outer-subscribe-with-complicated-inner-render`
2017-06-02 05:25:14 +00:00
pattern for a different purpose:
[Using Stateful JS Components](Using-Stateful-JS-Components.md)
2016-10-19 10:24:19 +00:00
Note this technique could be made simple and almost invisible via the
use of macros. (Contribute one if you have it).
This pattern has been independently discovered by many. For example, here
it is called the [Container/Component pattern](https://medium.com/@learnreact/container-components-c0e67432e005#.mb0hzgm3l).
## Summary
2017-07-14 04:43:51 +00:00
Event handlers will be your primary focus when testing. Remember to review the utilities in
2017-07-14 04:24:11 +00:00
[re-frame-test](https://github.com/Day8/re-frame-test).
2017-05-29 14:54:18 +00:00
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<!-- END doctoc generated TOC please keep comment here to allow auto update -->