-
-
Notifications
You must be signed in to change notification settings - Fork 716
Testing
This document is out of date.
A more modern version can be found within the repo
With a re-frame app, there's three things to test:
- Event handlers
- Subscription handlers
- Components
Event Handlers are the easiest of the three because they are pure functions.
First, create an event handler like this:
(defn my-handler
[db v]
... return a modified version of db)
Then, register it in a separate step:
(register-handler :some-id [some-middleware] my-handler)
With this setup, my-handler
is available for direct testing.
Your unittests will pass in certain values for db
and v
, and then ensure it returns the right (modified version of) db
.
Event handlers mutate the value in app-db
- that's their job.
I'd recommend defining a Prismatic Schema for the value in app-db
and then checking for correctness after every, single event handler. Every single one.
Using after
middleware, this is easy to arrange. The todomvc example shows how:
-
define a Schema for the value in
app-db
- create some middleware
- add the middleware to your event handlers
Here's a subscription handler from the todomvc example:
(register-sub
:completed-count
(fn [app-db _]
(reaction (completed-count (:todos @app-db)))))
How do we test this?
We could split the handler function from its registration, like this:
(defn get-completed-count
[app-db _]
(reaction (completed-count (:todos @app-db))))
(register-sub
:completed-count
get-completed-count)
That makes get-completed-count
available for direct testing. But you'll note it isn't a pure function. It isn't values in, values out. Instead, it is atoms in, atoms out.
If this function was on a paint chart, they'd call in "Arctic Fusion" to indicate its proximity to pure white, while hinting at taints.
We could accept this. We could create tests by passing in a reagent/atom
holding the certain values and then checking the values in what's returned. That would work. The more pragmatic among us might even approve.
But let's assume that's not good enough. Let's refactor for pureness:
The 1st step in this refactor is to create a pure function which actually does ALL the hard work ...
(defn completed-count-handler
[db v] ;; db is a value, not an atom
..... return a value here based on the values db and v)
The 2nd step in the refactor is to register using a thin reaction
wrapper:
(register-sub
:completed-count
(fn [app-db v]
(reaction (completed-count-handler @app-db v))))
Because completed-count-handler
is now doing all the work, it is the thing we want to test, and it is now a pure function. So I think we are there.
Components/views are slightly more tricky. There's a few options.
First, I have to admit an ugly secret. I don't tend to write tests for my views. Hey, don't give me that disproving frown! I have my reasons.
Remember that every line of code you write is a liability. So tests have to earn their keep - they have to deliver a good cost / benefit ratio. And, in my experience with the re-frame architecture, the Reagent view components tend to be an unlikely source of bugs. There's just not much logic in them for me to get wrong.
Okay, fine, don't believe me, then!!
Here's how, theoretically, I'd write tests if I wasn't me ...
If a Components is a Form-1 structure, then it is fairly easy to test.
A trivial example:
(defn greet
[name]
[:div "Hello " name])
(greet "Wiki")
;;=> [:div "Hello " "Wiki"]
So, here, testing involves passing values into the function and checking the data structure returned for correctness.
What's returned is hiccup, of course. So how do you test hiccup for correctness?
hiccup is just a clojure data structure - vectors containing keywords, and maps, and other vectors, etc. 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.
But what if the Component has a subscription (via a Form-2 structure)?
(defn my-view
[something]
(let [val (subscribe [:query-id])] <-- reactive subscription
(fn [something] <-- returns the render function
[:div .... using @val in here])))
There's no immediately obvious way to test this as a lovely pure function. Because it is not pure.
Of course, less pure ways are very possible. For example, a plan might be:
- setup
app-db
with some values in the right places (for the subscription) - call
my-view
(with a parameter) which will returnthe renderer
- call
the renderer
(with a parameter) which will return hiccup - check the hiccup structure for correctness.
Continuing on, in a second phase you could then:
5. change the value in app-db
(which will cause the subscription to fire)
6. call the renderer
again (hiccup returned).
7. check that the hiccup
Which is all possible, if a little messy, and with one gotcha. After you change the value in app-db
the subscription won't hold the new value straight away. It won't get calculated until the next animationFrame. And the next animationFrame won't happen until you hand back control to the browser. I think. Untested. Please report back here if you try. And you might also be able to use reagent.core/flush
to force the view to be updated.
Or ... instead of the not-very-pure method above, you could use with-redefs
on subscribe
to stub out re-frame altogether:
(defn subscription-stub [x]
(atom
(case x
[:query-id] 42)))
(deftest some-test
(with-redefs [re-frame/subscribe (subscription-stub)]
(testing "some rendering"
..... somehow call or render the component and check the output)))
For more integration level testing, you can use with-mounted-component
from the reagent-template to render the component in the browser and validate the generated DOM.
Or ... you can structure in the first place for easier testing and pure functions.
The trick here is to create an outer and inner component. The outer sources the data (via a subscription), and passes it onto the inner as props (parameters).
As a result, the inner component, which does the testable work, is pure and easily tested. The outer is fairly trivial.
To get a more concrete idea, I'll direct you to another page on this Wiki which has nothing to do with testing, but it does use this simple-outer-subscribe-with-complicated-inner-render
pattern for a different purpose: Using-Stateful-JS-Components
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.
So, we stumbled slightly at the final hurdle with Form-2 Components. But prior to this, the testing story for re-frame was as good as it gets: you are testing a bunch of simple, pure functions. No dependency injection in sight!
Deprecated Tutorials:
Reagent: