Skip to content

re-frame.test-support

re-frame.test-support is the runtime-state testing surface. It holds test-only fixture machinery and test-flavoured helpers that drive and assert against a frame's app-db, the registrar, the dispatch drain, and the trace stream. Its sibling re-frame.test-helpers owns the view-tree axis: the hiccup walkers plus the testid authoring helper.

This namespace does not re-export from re-frame.core, so a production build never picks up test-flavoured machinery by accident. A test file requires it alongside [re-frame.core :as rf] (and [re-frame.test-helpers :as th] for view assertions).

(:require [re-frame.test-support :as ts])

Examples below also use [re-frame.core :as rf] for the production primitives that double as testing entry points (dispatch-sync, app-db-value, …). For the practical how-to, see Test an event handler and Test a pipeline run.

Fixture machinery

The fixture primitives follow one pattern: snapshot the registrar before the test mutates registrations, then restore it afterwards, whether the test passes or fails. Framework-shipped registrations are captured in the snapshot, so they survive. Per-test registrations are rolled back.

snapshot-registrar

  • Kind: function
  • Signature:
    (snapshot-registrar)  snapshot
    
  • Description: Capture the current registrar state. Returns a snapshot value for later restore.
  • Example:
    ;; Capture before a test mutates registrations; roll back on the way out.
    (let [snap (ts/snapshot-registrar)]
      ;; ... test body registers extra handlers / subs ...
      (ts/restore-registrar! snap))
    

restore-registrar!

  • Kind: function
  • Signature:
    (restore-registrar! snapshot)  nil
    
  • Description: Restore a previously captured registrar snapshot. Returns nil.
  • Example:
    ;; `snap` was captured earlier via `snapshot-registrar`.
    (ts/restore-registrar! snap)
    

make-reset-runtime-fixture

  • Kind: function
  • Signature:
    (make-reset-runtime-fixture)
    (make-reset-runtime-fixture opts)  fixture-fn | {:before  :after }
    
  • Description: Build a clojure.test / cljs.test :each fixture that resets the per-process runtime around each test. Pair with use-fixtures :each.

    Around each test the fixture:

    • reinstates the stable ns-load registrar and source-store baseline captured at fixture-build time, which makes tests independent of run order inside a shared test bundle;
    • snapshots the registrar;
    • resets the frames registry, trace listeners, and per-artefact state (flows, schemas, machines, routing, resources, http, epoch). This runs via late-bind hooks that no-op when an artefact is absent from the classpath;
    • disposes then reinstalls the adapter;
    • restores everything in a finally.

    opts (all optional):

    Key Meaning
    :adapter Substrate adapter to install; also ensures the :rf/default frame. When omitted, no adapter is installed.
    :app-ns Bundle co-load hygiene. A provenance-namespace PREFIX string naming this suite's own app ("realworld-http." — the whole tree, not one ns; never a sibling's). Rows whose :rf.provenance/ns starts with it are captured and removed from the live registrar and the source store when the fixture is built — before it takes its baselines, so no suite's baseline holds them — and reinstated through registrar/register! before each test, after the reset and before :init-fn. The ordinary source-store restore takes them out again, on the exceptional path too. Omit it unless your bundle co-loads rival apps.
    :init-fn Zero-arg fn run after adapter install, before the test body, under the same ambient frame scope as the body.
    :clear-kinds Collection of registrar kinds cleared after the snapshot capture and before the body (the snapshot restores them on the way out).
    :clear-app-schemas? Boolean; clear the schemas artefact's per-frame side-table for the test's duration.
    :ambient-frame Frame id bound as the body's ambient scope when an adapter is installed. Default :rf/default; pass nil to opt out (for tests that create their own top-level frames).
    :async? Boolean, default false. Declares the suite async-capable; the return shape that delivers it is chosen per host. On CLJS you get a cljs.test map-form fixture {:before … :after …}, required for suites with (async done …) tests. On the JVM the option is inert and you always get the fn-form — clojure.test has no async tests, and no map-fixture support at all (it invokes a fixture, and a Clojure map is IFn, so a map fixture would silently skip every test body).
  • Example:

    (use-fixtures :each
      (ts/make-reset-runtime-fixture {:adapter plain-atom/adapter}))
    
    ;; CLJS suite with (async done …) tests — map-form fixture:
    (use-fixtures :each
      (ts/make-reset-runtime-fixture {:adapter plain-atom/adapter :async? true}))
    

    A .cljc suite whose CLJS rows are async writes the same plain :async? true — no reader conditional at the call site, because the factory already picks the map on CLJS and the fn-form on the JVM.

    :app-ns — when your bundle co-loads more than one app. A CLJS node runner loads every test namespace into one bundle before any test runs. Two co-loaded apps that register the same per-app id — :rf.route/not-found, or shared event vocabulary — leave duplicate provenance rows in the source store, and default-image assembly then fails loud with :rf.error/image-duplicate-id for any suite whose baseline was captured after the second app loaded. :app-ns folds the whole capture/reinstate cycle into the fixture that already owns the baseline:

    (use-fixtures :each
      (ts/make-reset-runtime-fixture
        {:adapter reagent-adapter/adapter
         :app-ns  "my-app."          ; rows under this provenance prefix are MINE
         :init-fn init!}))
    

    Name your own app's root namespace and cover its whole tree — never a sibling's. When every app suite hides itself, no suite needs to know its sibling's name, and the suites are independent of each other's load order. A workspace with one app in its bundle never meets the collision and never needs this key.

Test-flavoured helpers

To fire several events in order, call rf/dispatch-sync per event — each drains to fixed point before the next, so observable state between calls reflects committed effects:

(doseq [ev [[:counter/inc] [:counter/inc] [:counter/dec]]]
  (rf/dispatch-sync ev))

assert-path-equals

  • Kind: function
  • Signature:
    (assert-path-equals path expected-val)
    (assert-path-equals path expected-val opts)
    
  • Description: Assert (get-in db path) == expected-val against the resolved frame's app-db. A mismatch fires a clojure.test/is-style failure via do-report. Returns true on pass and false otherwise; the failure has already been reported either way.

    opts: :frame targets a non-default frame; frame resolution is :frame opt → (current-frame):rf/default.

    This is the fn-side counterpart to the :rf.assert/path-equals story event-family: same name root, different runner channel.

  • Example:

    (rf/dispatch-sync [:counter/inc])
    (ts/assert-path-equals [:n] 1)
    ;; against a non-default frame:
    (ts/assert-path-equals [:cart :count] 2 {:frame :checkout})
    

For a full-db assertion, compare directly: (is (= expected-db (rf/app-db-value frame-id))).

Deterministic-wait helpers

poll-until

  • Kind: function
  • Signature:
    (poll-until pred)
    (poll-until pred opts)
    
  • Description: Poll pred until it returns truthy, within a bounded deadline.

    • JVM: synchronous. Returns the truthy value, or throws ex-info on timeout.
    • CLJS: returns a js/Promise that resolves with the truthy value, or rejects on timeout. A pred that returns a js/Promise is awaited; its resolved value drives the truthy check.

    The timeout error carries :rf.error/id :rf.error/poll-until-timeout (the canonical discriminator), plus :elapsed-ms and :label in its data.

    opts: :timeout-ms (default 2000), :interval-ms (default 5), :label.

  • Example:

    ;; JVM — synchronous; returns the truthy value (throws on timeout).
    (ts/poll-until #(= 2 (:n (rf/app-db-value :rf/default)))
                   {:label "counter reached 2"})
    
    ;; CLJS — returns a js/Promise; compose with cljs.test/async.
    (-> (ts/poll-until #(= 3 (:n (rf/app-db-value :rf/default))))
        (.then (fn [_] (done))))
    

Trace-recorder bracket

with-trace-recorder!

  • Kind: macro
  • Signature:
    (with-trace-recorder! [recs-sym] body+)
    (with-trace-recorder! [recs-sym opts] body+)
    
  • Description: Bracket body with a fresh trace-tooling listener that accumulates matching trace events into an atom bound to recs-sym. The listener is registered before body runs and unregistered in a finally on the way out, even if body throws. Returns the value of body's final form.

    opts (optional map literal; keys evaluated at macroexpansion):

    • :pred — a 1-arg (fn [ev] truthy?) filter. Default: accept every event.
    • :shape:flat (default; the atom holds a vector of events) or :by-op (the atom holds a map keyed by (:operation ev)).
    • :key — listener key. Default: a freshly-gensym'd keyword unique to the expansion site, so two brackets in one test do not collide.

    Macro requires:

    • JVM: resolves alias-qualified through the normal (:require [re-frame.test-support :as ts]).
    • CLJS: the namespace carries no self-:require-macros (unlike re-frame.core). CLJS test files must therefore require the macro explicitly: (:require-macros [re-frame.test-support :refer [with-trace-recorder!]]), or :as ts in :require-macros for alias-qualified use.
    • Example:
      ;; Flat shape (default), default filter, simple read.
      (ts/with-trace-recorder! [traces]
        (rf/dispatch-sync [:my-event])
        (is (= 1 (count (filter #(= :rf.event/run-start (:operation %))
                                @traces)))))
      
      ;; :by-op shape with a :pred filter — atom holds a map keyed by operation.
      (ts/with-trace-recorder! [observed
                                {:pred  #(contains? #{:rf.view/render
                                                      :rf.view/rendered}
                                                    (:operation %))
                                 :shape :by-op}]
        (render-twice!)
        (is (= 2 (count (:rf.view/render @observed))))
        (is (= 2 (count (:rf.view/rendered @observed)))))
      

Always-on emit-recorder bracket

with-emit-recorder!

  • Kind: macro
  • Signature:
    (with-emit-recorder! [recs-sym] body+)
    (with-emit-recorder! [recs-sym opts] body+)
    
  • Description: The always-on sibling of with-trace-recorder!. Brackets body with a fresh listener on one of the two always-on substrates, accumulating records into an atom bound to recs-sym. Registered before body runs, unregistered in a finally on the way out, even if body throws. Returns the value of body's final form.

    opts (optional map literal; keys evaluated at macroexpansion):

    • :stream:errors (default; brackets re-frame.error-emit, one record per promoted :rf.error/*) or :events (brackets re-frame.event-emit, one record per processed event).
    • :pred — a 1-arg (fn [record] truthy?) filter. Default: accept every record.
    • :key — listener key. Default: a freshly-gensym'd keyword unique to the expansion site, so two brackets in one test do not collide.

    These are the framework's own registries, not an application surface. re-frame.event-emit and re-frame.error-emit carry no public registration verb: rf2-kuky.69 retired the register-listener! :events / :errors streams, because an unprojected fan-out no frame's policy governs was a second, fail-open production door beside the projected one. They survive as implementation-tier seams for two consumers — the framework's own synchronous-window capture sites, and tests. An application observes production records through a frame's :observability sink, or the (rf/configure! {:observability …}) process default, both of which deliver a projected record. A test brackets the raw substrate on purpose: it wants the unprojected shape, inside a window it owns.

    Macro requires: the same shape as with-trace-recorder! above — JVM refers it through the ordinary :require; CLJS test files reach it with (:require-macros [re-frame.test-support :refer [with-emit-recorder!]]).

  • Example:

    ;; Default stream (:errors) — capture what one dispatch fails with.
    (ts/with-emit-recorder! [errs]
      (rf/dispatch-sync [:boom])
      (is (= [:rf.error/handler-exception] (mapv :error @errs))))
    
    ;; The event substrate, filtered to one frame.
    (ts/with-emit-recorder! [seen {:stream :events
                                   :pred   #(= :app/main (:frame %))}]
      (rf/dispatch-sync [:tick])
      (is (= 1 (count @seen))))
    

See also

  • re-frame.core.md — the production primitives that double as testing entry points (make-frame, with-frame, dispatch-sync, with-fx-overrides, app-db-value, compute-sub) and the registrar-introspection API (registrations, handler-meta).
  • re-frame.test-helpers.md — the sibling view-tree assertion namespace (hiccup walkers plus the testid authoring helper).
  • re-frame.http.md — HTTP test stubs for tests that exercise managed requests.
  • Test an event handler and Test a pipeline run — the practical how-to guides for the testing surface.