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."
Project maturity: CLJ is stable, CLJS is experimental, bb is experimental.
{:deps {com.hyperfiddle/rcf {:mvn/version "20220926-202227"}}}
Changelog
- :throws
- babashka support (experimental)
- breaking don't return final result return nil like comment
20220926-202227
!
is deprecated, usetap
instead20220827-151056
async test forms no longer guaranteed return final result20220405
maven group-id renamed fromhyperfiddle
tocom.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
Current dev priority is improving complex async tests in ClojureScript.
(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