RCF – a REPL-first, async test macro for Clojure/Script
Clojure
301
227 commits
updated Jun 26, 2026
RCF turns your Rich Comment Forms into tests (in the same file as your functions). Send form or file to REPL to run tests and it squirts dopamine ✅✅✅. It's good, try it!

Features
RCF is specifically engineered to support Electric Clojure, which we test, document and teach with RCF.
Hype quotes:
Project maturity: CLJ is stable, CLJS is stable, bb is experimental.
{:deps {com.hyperfiddle/rcf {:mvn/version "20260626-131050"}}}
Changelog
20260626-131050 fixed issues in complex cljs tests (cljs async % now sequences across all control flow, delegated to Cloroutine), cljs is now stabledo, return nil like comment20220926-202227 ! is deprecated, use tap instead20220827-151056 async test forms no longer guaranteed return final result20220405 maven group-id renamed from hyperfiddle to com.hyperfiddle for securityKnown issues
rcf/*enabled* to be true at both macroexpansion time and runtime. Let us know if you can figure out a way to improve this.(tests) blocks erase by default (macroexpanding to nothing), which avoids a startup time performance penalty as well as keeps tests out of prod.
It's an easy one-liner to turn on tests in your dev entrypoint:
(ns user ; user ns is loaded by REPL startup
(:require [hyperfiddle.rcf]))
(hyperfiddle.rcf/enable!)
Tests are run when you send a file or form to your Clojure/Script REPL.
(ns example
(:require [hyperfiddle.rcf :refer [tests tap %]]))
(tests
"equality"
(inc 1) := 2
"wildcards"
{:a :b, :b [2 :b]} := {:a _, _ [2 _]}
"unification"
{:a :b, :b [2 :b]} := {:a ?b, ?b [2 ?b]}
"unification on reference types"
(def x (atom nil))
{:a x, :b x} := {:a ?x, :b ?x}
"multiple tests on one value"
(def xs [:a :b :c])
(count xs) := 3
(last xs) := :c
(let [xs (map identity xs)]
(last xs) := :c
(let [] (last xs) := :c))
"exceptions"
(assert false "boom") :throws java.lang.AssertionError
(tests
"nested tests (is there a strong use case?)"
1 := 1)
(tests
"REPL bindings work"
(keyword "a") := :a
(keyword "b") := :b
(keyword "c") := :c
*1 := :c
*2 := :b
*3 := :a
*1 := :c ; inspecting history does not affect history
(keyword "d") := :d
*1 := :d
*2 := :c
*3 := :b
(symbol *2) := 'c ; this does affect history
(symbol *2) := 'd))
Loading src/example.cljc...
✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅Loaded
(ns example
(:require [clojure.core.async :refer [chan >! go go-loop <! timeout close!]]
[hyperfiddle.electric :as e]
[hyperfiddle.rcf :as rcf :refer [tests tap % with]]
[missionary.core :as m]))
(rcf/set-timeout! 100)
(tests
"async tests"
#?(:clj (tests
(future
(tap 1) (Thread/sleep 10) ; tap value to queue
(tap 2) (Thread/sleep 200)
(tap 3))
% := 1 ; pop queue
% := 2
% := ::rcf/timeout)
:cljs (tests
(defn setTimeout [f ms] (js/setTimeout ms f))
(tap 1) (setTimeout 10 (fn []
(tap 2) (setTimeout 200 (fn []
(tap 3)))))
% := 1
% := 2
% := ::rcf/timeout)))
(tests
"electric"
(def !x (atom 0))
(def dispose
(e/run
(let [x (e/watch !x)
a (inc x)
b (inc x)]
(tap (+ a b)))))
% := 2
(swap! !x inc)
% := 4
(swap! !x inc)
% := 6
(dispose))
(tests
"core.async"
(def c (chan))
(go-loop [x (<! c)]
(when x
(<! (timeout 10))
(tap x)
(recur (<! c))))
(go (>! c :hello) (>! c :world))
% := :hello
% := :world
(close! c))
(tests
"missionary"
(def !x (atom 0))
(def dispose ((m/reactor (m/stream! (m/ap (! (inc (m/?< (m/watch !x)))))))
(fn [_] #_(prn ::done)) #(prn ::crash %)))
% := 1
(swap! !x inc)
(swap! !x inc)
% := 2
% := 3
(dispose)))
To run in CI, configure a JVM flag for RCF to generate clojure.test deftests, and then run them with clojure.test. Github actions example.
; deps.edn
{:aliases {:test {:jvm-opts ["-Dhyperfiddle.rcf.generate-tests=true"]}}}
% clj -M:test -e "(require 'example)(clojure.test/run-tests 'example)"
Testing example
✅✅✅✅✅✅✅✅
Ran 1 tests containing 8 assertions.
0 failures, 0 errors.
{:test 1, :pass 8, :fail 0, :error 0, :type :summary}
For CLJS tests to run, rcf/enable! must be true in both CLJ (shadow-cljs macroexpansion time) and CLJS (JS runtime). Reports may be printed to browser console instead of the REPL, because browser REPLs donn't intercept the async println.
(ns dev-entrypoint
(:require [example] ; transitive inline tests will erase
[hyperfiddle.rcf :refer [tests]]))
; wait to enable tests until after app namespaces are loaded
(hyperfiddle.rcf/enable!)
; subsequent REPL interactions will run tests
; prevent test execution during cljs hot code reload
#?(:cljs (defn ^:dev/before-load stop [] (hyperfiddle.rcf/enable! false)))
#?(:cljs (defn ^:dev/after-load start [] (hyperfiddle.rcf/enable!)))
One of my tests threw an exception, but the stack trace is empty? — you want {:jvm-opts ["-XX:-OmitStackTraceInFastThrow"]} explanation (this may be JVM specific)
I see no output — RCF is off by default, run (hyperfiddle.rcf/enable!)
Emacs has no output and tests are enabled — check if your emacs supports emojis
How do I customize what’s printed at the REPL? — see reporters.clj, reporters.cljs
#hyperfiddle @ clojurians.net

Clojure
98.5%
RCF – a REPL-first, async test macro for Clojure/Script
Clojure
301
227 commits
updated Jun 26, 2026
RCF turns your Rich Comment Forms into tests (in the same file as your functions). Send form or file to REPL to run tests and it squirts dopamine ✅✅✅. It's good, try it!

Features
RCF is specifically engineered to support Electric Clojure, which we test, document and teach with RCF.
Hype quotes:
Project maturity: CLJ is stable, CLJS is stable, bb is experimental.
{:deps {com.hyperfiddle/rcf {:mvn/version "20260626-131050"}}}
Changelog
20260626-131050 fixed issues in complex cljs tests (cljs async % now sequences across all control flow, delegated to Cloroutine), cljs is now stabledo, return nil like comment20220926-202227 ! is deprecated, use tap instead20220827-151056 async test forms no longer guaranteed return final result20220405 maven group-id renamed from hyperfiddle to com.hyperfiddle for securityKnown issues
rcf/*enabled* to be true at both macroexpansion time and runtime. Let us know if you can figure out a way to improve this.(tests) blocks erase by default (macroexpanding to nothing), which avoids a startup time performance penalty as well as keeps tests out of prod.
It's an easy one-liner to turn on tests in your dev entrypoint:
(ns user ; user ns is loaded by REPL startup
(:require [hyperfiddle.rcf]))
(hyperfiddle.rcf/enable!)
Tests are run when you send a file or form to your Clojure/Script REPL.
(ns example
(:require [hyperfiddle.rcf :refer [tests tap %]]))
(tests
"equality"
(inc 1) := 2
"wildcards"
{:a :b, :b [2 :b]} := {:a _, _ [2 _]}
"unification"
{:a :b, :b [2 :b]} := {:a ?b, ?b [2 ?b]}
"unification on reference types"
(def x (atom nil))
{:a x, :b x} := {:a ?x, :b ?x}
"multiple tests on one value"
(def xs [:a :b :c])
(count xs) := 3
(last xs) := :c
(let [xs (map identity xs)]
(last xs) := :c
(let [] (last xs) := :c))
"exceptions"
(assert false "boom") :throws java.lang.AssertionError
(tests
"nested tests (is there a strong use case?)"
1 := 1)
(tests
"REPL bindings work"
(keyword "a") := :a
(keyword "b") := :b
(keyword "c") := :c
*1 := :c
*2 := :b
*3 := :a
*1 := :c ; inspecting history does not affect history
(keyword "d") := :d
*1 := :d
*2 := :c
*3 := :b
(symbol *2) := 'c ; this does affect history
(symbol *2) := 'd))
Loading src/example.cljc...
✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅Loaded
(ns example
(:require [clojure.core.async :refer [chan >! go go-loop <! timeout close!]]
[hyperfiddle.electric :as e]
[hyperfiddle.rcf :as rcf :refer [tests tap % with]]
[missionary.core :as m]))
(rcf/set-timeout! 100)
(tests
"async tests"
#?(:clj (tests
(future
(tap 1) (Thread/sleep 10) ; tap value to queue
(tap 2) (Thread/sleep 200)
(tap 3))
% := 1 ; pop queue
% := 2
% := ::rcf/timeout)
:cljs (tests
(defn setTimeout [f ms] (js/setTimeout ms f))
(tap 1) (setTimeout 10 (fn []
(tap 2) (setTimeout 200 (fn []
(tap 3)))))
% := 1
% := 2
% := ::rcf/timeout)))
(tests
"electric"
(def !x (atom 0))
(def dispose
(e/run
(let [x (e/watch !x)
a (inc x)
b (inc x)]
(tap (+ a b)))))
% := 2
(swap! !x inc)
% := 4
(swap! !x inc)
% := 6
(dispose))
(tests
"core.async"
(def c (chan))
(go-loop [x (<! c)]
(when x
(<! (timeout 10))
(tap x)
(recur (<! c))))
(go (>! c :hello) (>! c :world))
% := :hello
% := :world
(close! c))
(tests
"missionary"
(def !x (atom 0))
(def dispose ((m/reactor (m/stream! (m/ap (! (inc (m/?< (m/watch !x)))))))
(fn [_] #_(prn ::done)) #(prn ::crash %)))
% := 1
(swap! !x inc)
(swap! !x inc)
% := 2
% := 3
(dispose)))
To run in CI, configure a JVM flag for RCF to generate clojure.test deftests, and then run them with clojure.test. Github actions example.
; deps.edn
{:aliases {:test {:jvm-opts ["-Dhyperfiddle.rcf.generate-tests=true"]}}}
% clj -M:test -e "(require 'example)(clojure.test/run-tests 'example)"
Testing example
✅✅✅✅✅✅✅✅
Ran 1 tests containing 8 assertions.
0 failures, 0 errors.
{:test 1, :pass 8, :fail 0, :error 0, :type :summary}
For CLJS tests to run, rcf/enable! must be true in both CLJ (shadow-cljs macroexpansion time) and CLJS (JS runtime). Reports may be printed to browser console instead of the REPL, because browser REPLs donn't intercept the async println.
(ns dev-entrypoint
(:require [example] ; transitive inline tests will erase
[hyperfiddle.rcf :refer [tests]]))
; wait to enable tests until after app namespaces are loaded
(hyperfiddle.rcf/enable!)
; subsequent REPL interactions will run tests
; prevent test execution during cljs hot code reload
#?(:cljs (defn ^:dev/before-load stop [] (hyperfiddle.rcf/enable! false)))
#?(:cljs (defn ^:dev/after-load start [] (hyperfiddle.rcf/enable!)))
One of my tests threw an exception, but the stack trace is empty? — you want {:jvm-opts ["-XX:-OmitStackTraceInFastThrow"]} explanation (this may be JVM specific)
I see no output — RCF is off by default, run (hyperfiddle.rcf/enable!)
Emacs has no output and tests are enabled — check if your emacs supports emojis
How do I customize what’s printed at the REPL? — see reporters.clj, reporters.cljs
#hyperfiddle @ clojurians.net

Clojure
98.5%