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).
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:
- Description: Capture the current registrar state. Returns a snapshot value for later restore.
- Example:
restore-registrar!¶
- Kind: function
- Signature:
- Description: Restore a previously captured registrar
snapshot. Returnsnil. - Example:
make-reset-runtime-fixture¶
- Kind: function
- Signature:
-
Description: Build a
clojure.test/cljs.test:eachfixture that resets the per-process runtime around each test. Pair withuse-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 :adapterSubstrate adapter to install; also ensures the :rf/defaultframe. When omitted, no adapter is installed.:app-nsBundle 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/nsstarts 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 throughregistrar/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-fnZero-arg fn run after adapter install, before the test body, under the same ambient frame scope as the body. :clear-kindsCollection 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-frameFrame id bound as the body's ambient scope when an adapter is installed. Default :rf/default; passnilto 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 acljs.testmap-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.testhas no async tests, and no map-fixture support at all (it invokes a fixture, and a Clojure map isIFn, 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
.cljcsuite 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-idfor any suite whose baseline was captured after the second app loaded.:app-nsfolds 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:
assert-path-equals¶
- Kind: function
- Signature:
-
Description: Assert
(get-in db path) == expected-valagainst the resolved frame'sapp-db. A mismatch fires aclojure.test/is-style failure viado-report. Returnstrueon pass andfalseotherwise; the failure has already been reported either way.opts::frametargets a non-default frame; frame resolution is:frameopt →(current-frame)→:rf/default.This is the fn-side counterpart to the
:rf.assert/path-equalsstory event-family: same name root, different runner channel. -
Example:
For a full-db assertion, compare directly: (is (= expected-db (rf/app-db-value frame-id))).
Deterministic-wait helpers¶
poll-until¶
- Kind: function
- Signature:
-
Description: Poll
preduntil it returns truthy, within a bounded deadline.- JVM: synchronous. Returns the truthy value, or throws
ex-infoon timeout. - CLJS: returns a
js/Promisethat resolves with the truthy value, or rejects on timeout. Apredthat returns ajs/Promiseis 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-msand:labelin its data.opts::timeout-ms(default 2000),:interval-ms(default 5),:label. - JVM: synchronous. Returns the truthy value, or throws
-
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:
-
Description: Bracket
bodywith a fresh trace-tooling listener that accumulates matching trace events into an atom bound torecs-sym. The listener is registered beforebodyruns and unregistered in afinallyon the way out, even ifbodythrows. Returns the value ofbody'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(unlikere-frame.core). CLJS test files must therefore require the macro explicitly:(:require-macros [re-frame.test-support :refer [with-trace-recorder!]]), or:as tsin:require-macrosfor 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:
-
Description: The always-on sibling of
with-trace-recorder!. Bracketsbodywith a fresh listener on one of the two always-on substrates, accumulating records into an atom bound torecs-sym. Registered beforebodyruns, unregistered in afinallyon the way out, even ifbodythrows. Returns the value ofbody's final form.opts(optional map literal; keys evaluated at macroexpansion)::stream—:errors(default; bracketsre-frame.error-emit, one record per promoted:rf.error/*) or:events(bracketsre-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-emitandre-frame.error-emitcarry no public registration verb: rf2-kuky.69 retired theregister-listener!:events/:errorsstreams, 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:observabilitysink, 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
testidauthoring 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.