Test a subscription¶
A subscription computes a value from app-db. rf/compute-sub runs a sub against an app-db value and returns the result. It runs on the JVM with no Reagent, no installed adapter and no subscription cache.
The subs under test are the todo chain from Subscriptions: :todo/todos and :todo/showing read app-db, :todo/all turns the map into a sorted vector, and :todo/visible filters it.
;; src/my_app/subs.cljc
(rf/reg-sub :todo/todos (fn [db _] (:todos db)))
(rf/reg-sub :todo/showing (fn [db _] (:showing db)))
(rf/reg-sub :todo/all {:inputs [[:todo/todos]]}
(fn [[todos] _]
(vec (sort-by :id (vals todos)))))
(rf/reg-sub :todo/visible
{:inputs [[:todo/all] [:todo/showing]]}
(fn [[todos showing] _]
(case showing
:active (filterv (complement :done?) todos)
:done (filterv :done? todos)
todos)))
(ns my-app.subs-test
(:require [clojure.test :refer [deftest is use-fixtures]]
[re-frame.core :as rf]
[re-frame.test-support :as ts]
[re-frame.substrate.plain-atom :as plain-atom] ;; the headless adapter frames need
[my-app.subs])) ;; loading the ns registers the subs
(use-fixtures :each (ts/make-reset-runtime-fixture {:adapter plain-atom/adapter}))
(deftest visible-honours-showing
(let [db {:todos {1 {:id 1 :title "Buy milk" :done? true}
2 {:id 2 :title "Walk the dog" :done? false}}
:showing :active}]
(is (= ["Walk the dog"]
(mapv :title (rf/compute-sub [:todo/visible] db))))))
The test never computes :todo/todos, :todo/all or :todo/showing. Given the query vector and a db, compute-sub resolves the whole input chain in dependency order. It keeps no cache between calls, so the same query and db always return the same value. A sub that takes arguments tests the same way, with the arguments in the query vector: (rf/compute-sub [:todo/by-id 2] db).
Build the db with real events¶
A hand-written db literal encodes the shape of app-db. For a simple extractor that is fine. For a sub that depends on the shape your events produce, the literal can go stale without failing: when a handler changes that shape, the test keeps passing against a db your app no longer builds.
Instead, boot a test frame with :initial-events and compute the sub against the db those events produce:
;; :todo/add and :todo/toggle are the handlers from Test an event handler;
;; :todo/set-showing does (assoc db :showing kw).
(deftest visible-after-events
(rf/with-new-frame [f (rf/make-frame
{:fx-overrides {:todo.storage/save (fn [_ _] nil)}
:initial-events [[:todo/add "Buy milk"]
[:todo/add "Walk the dog"]
[:todo/toggle 1]
[:todo/set-showing :active]]})]
(is (= ["Walk the dog"]
(mapv :title (rf/compute-sub [:todo/visible] (rf/app-db-value f)))))))
Use a literal db when the sub is trivial and the shape is obvious, and seed with events when the shape matters. Either way, don't use subscribe plus deref in tests: it needs a live cache and an installed adapter, and adds nothing to a value assertion.
compute-sub checks the computation, not the reactive machinery: it proves the value is right, not that a view re-renders when it changes. Change propagation is the framework's job, so you don't re-test it per sub. When the thing under test is "the view updated", write a view test.
One diagnostic differs from a live subscription: an unregistered sub or input
computes to nil without a :rf.error/no-such-sub record. Live subscribe reports
that missing registration. If a value assertion unexpectedly returns nil, check
the input ids and require list as well as the sub's computation.
When the sub carries a :schema¶
Your assertions don't change. A sub registered with an output :schema validates its value in dev, compute-sub included. A shape bug emits a :rf.error/schema-validation-failure error record, and the call returns nil. Asserting on that error record is an ordinary listener-based test.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
compute-sub returns nil unexpectedly |
The sub body threw (:rf.error/sub-exception), its output failed its :schema, or an input names an unregistered sub (which computes to nil with no error record) |
Check your error listener, then the input ids and the require list |
| Test passes but the app shows something else | The literal db no longer matches the shape your handlers build |
Seed the db with :initial-events |
If a hand-rolled fixture or a REPL session does use the live cache, (rf/clear-sub-cache! frame-id) disposes every cached subscription for that frame. The reset fixture already discards frames between tests.