Skip to content

Testing

re-frame2's payoff at test time is one structural fact: the interesting parts of your app are pure functions, so you test them as functions — no browser, no DOM, no mocking framework standing between you and the assertion. A handler is a function you call with maps; a subscription is a function you call with a db value; a view returns hiccup you walk as data; and a whole pipeline run replays deterministically because everything it consumed was recorded. These tests run on the JVM in milliseconds.

Test the pure middle as functions; control the impure edges at their seams.

The pages here climb one rung at a time, and each stands alone — start wherever your test is:

Test… The move Page
an event handler pluck it from the registrar, call it with literal coeffects, check the returned map Event handlers
a subscription compute-sub against a db value — the whole :<- chain resolves for you Subscriptions
a view call it, walk the returned hiccup with re-frame.test-helpers Views
a whole pipeline run dispatch-sync into a fresh frame with supplied facts and canned replies Pipeline runs

One habit runs through every page: setup rides the frame's construction; the body dispatches only the action under test. make-frame takes :initial-events — the same ordered event script a production frame boots with — so the state a test needs before its action is declared where the frame is made, not stacked up as dispatch-sync calls above the assertion. A setup step that needs pinned facts takes the map form, {:event [...] :opts {:rf.cofx {...}}}, and a failing step tears the frame down loudly instead of leaving the test half-seeded. When you see a dispatch-sync in a test body on these pages, it's the event being tested.

What about reg-fx and reg-cofx?

They don't get pages here, and that's deliberate: they are the app's designed impure edges — a reg-fx body touches the host, a reg-cofx supplier reads it — so "unit-test it as a pure function" doesn't apply. What you test instead is everything around them, at the seams the framework gives you:

  • Coeffects are supplied as data{:rf.cofx {:rf/time-ms …}} on the dispatch pins any world fact a handler declared, no supplier involved. Pipeline runs covers it.
  • Effects are redirected as data:fx-overrides captures or stubs any app fx-id for one dispatch (or a whole frame) — a handful of state-installing reserved fxs excepted, see Pipeline runs — so you assert on the exact args map your handler built without performing anything.
  • An edge with real logic in its body is still just a two-arg function — call it directly with a stub frame context when it earns a test — but keep those bodies thin on purpose: the thinner the edge, the more of its behaviour the seam tests above already cover.

The neighbours

Each capability tab carries its own testing page, built on the same moves: Machines (a transition is a pure function call), Routing (a URL codec you call, a guard flow you drive with zero DOM), Resources (canned replies in, cache projections out), and SSR (your server tests are just JVM tests). Setting up the runner itself — the deps.edn :test alias, and the .cljc discipline that lets your registration namespaces load on the JVM — is walked in the tutorial's Part 5: test it, ship it.