Skip to content

Repository files navigation

RCF – REPL-first async test macro for Clojure/Script

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

  • Clojure/Script
  • async tests
  • zero boilerplate
  • natural REPL workflow
  • one key-chord to run tests, no hotkey configuring
  • same-file tests (examples are better than docstrings)
  • no file watchers, no extra windows, no beeping, no latency
  • notebook support – example NextJournal notebook
  • it's fun! ✅✅✅

RCF is specifically engineered to support Electric Clojure, which we test, document and teach with RCF.

Hype quotes:

  • "RCF has changed my habits with regards to tests. It is so much easier than flipping back and forth between files, you get my preferred work habits - work in a comment block until something works. But before RCF I never took the time to turn comment blocks into an automated test"
  • "I use RCF to do leetcode style questions as 'fun practice.' It certainly didn't feel fun before!"
  • "I think people make the mistake of comparing this with other methods of inlining tests near their function definitions. The integration with the REPL, low syntax/interface, reduces friction and makes testing more attractive as a language of communication and verification."
  • "I used RCF in a successful interview. RCF was a massive help in communication and a fast tool for thought whilst under the conditions of technical interview."

Dependency

Project maturity: CLJ is stable, CLJS is stable, bb is experimental.

{:deps {com.hyperfiddle/rcf {:mvn/version"20260626-131050"}}}

JVMNodeJSBrowser

Changelog

  • 20260626-131050 fixed issues in complex cljs tests (cljs async % now sequences across all control flow, delegated to Cloroutine), cljs is now stable
  • :throws
  • babashka support (experimental)
  • breaking don't return final result as if by do, return nil like comment
  • 20220926-202227! is deprecated, use tap instead
  • 20220827-151056 async test forms no longer guaranteed return final result
  • 20220405 maven group-id renamed from hyperfiddle to com.hyperfiddle for security
  • 2021 Dec 18: clojurescript dependency is now under the :cljs alias, see #25
  • 2021 Oct 20: custom reporters now dispatch on qualified keywords, see #19

Known issues

  • CLJS configuration is fiddly due to needing rcf/*enabled* to be true at both macroexpansion time and runtime. Let us know if you can figure out a way to improve this.

Usage

(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:

(nsuser; 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.

(nsexample
(:require [hyperfiddle.rcf :refer [tests tap %]]))
(tests"equality"
(inc1) :=2"wildcards"
{:a:b, :b [2:b]} := {:a _, _ [2 _]}
"unification"
{:a:b, :b [2:b]} := {:a ?b, ?b [2 ?b]}
"unification on reference types"
(defx (atomnil))
{:a x, :b x} := {:a ?x, :b ?x}
"multiple tests on one value"
(defxs [:a:b:c])
(count xs) :=3
(last xs) :=:c
(let [xs (map identity xs)]
(last xs) :=:c
(let [] (last xs) :=:c))
"exceptions"
(assertfalse"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

Async tests

(nsexample
(: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
(tap1) (Thread/sleep10) ; tap value to queue
(tap2) (Thread/sleep200)
(tap3))
% :=1; pop queue
% :=2
% :=::rcf/timeout)
:cljs (tests
(defnsetTimeout [f ms] (js/setTimeout ms f))
(tap1) (setTimeout10 (fn []
(tap2) (setTimeout200 (fn []
(tap3)))))
% :=1
% :=2
% :=::rcf/timeout)))
(tests"electric"
(def!x (atom0))
(defdispose
(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"
(defc (chan))
(go-loop [x (<! c)]
(when x
(<! (timeout10))
(tap x)
(recur (<! c))))
(go (>! c :hello) (>! c :world))
% :=:hello
% :=:world
(close! c))
(tests"missionary"
(def!x (atom0))
(defdispose ((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)))

CI

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}

ClojureScript configuration

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.

(nsdev-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!)))

FAQ

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

Community

#hyperfiddle @ clojurians.net

Scroll Of Truth meme saying "you do not really understand something until you can explain it as a passing test".

About

RCF – a REPL-first, async test macro for Clojure/Script

Topics

Resources

Stars

302 stars

Watchers

9 watching

Forks

Releases

Packages

Used by

Contributors

Languages