Liking cljdoc? Tell your friends :D

RCF – asynchronous Rich Comment Forms and REPL-first 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
  • No file watchers, no extra windows, no beeping, no latency
  • One key-chord to run tests, no hotkey configuring

Deeper goal: a notation for communication

  • Documentation tool. RCF lets you share example usages next to the source code of the function (which is way better than docstrings). Figuring out what dense Clojure code does is actually really hard and RCF fixes that. Example nextjournal notebook documentation using RCF
  • Pair programming tool. While pairing on Zoom, bang out some assertions quickly, right in the file you're working on. Watch your communication bandwidth improve.
  • Teaching tool. RCF helps beginners experiment and check their work.

RCF is specifically engineered to support hyperfiddle/photon, our upcoming reactive dialect of Clojure, that 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 think people make the mistake of comparing this with other methods of inlining tests near their function definitions (which has been possible, though uncommon, for a long time). 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."
  • "I use RCF to do leetcode style questions as 'fun practice.' It certainly didn't feel fun before!"

Dependency

Project maturity: CLJ is stable, external users. CLJS is alpha

; stable
{:deps {com.hyperfiddle/rcf {:mvn/version "20220902-130636"}}}

Breaking changes:

  • :mvn/version "20220827-151056" test forms no longer return final result
  • :mvn/version "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

Experimental (master): the current development priority is improving complex async tests in ClojureScript, DX and some experiments with unification.

; development - not currently published
;{:deps {com.hyperfiddle/rcf {:git/url "https://github.com/hyperfiddle/rcf.git" :sha ...}}}

JVM NodeJS Browser

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:

(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. In Cursive, that's cmd-shift-L to load the file.

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

  (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

(ns example
  (:require [clojure.core.async :refer [chan >! go go-loop <! timeout close!]]
            [hyperfiddle.rcf :as rcf :refer [tests tap %]]
            [missionary.core :as m]))

(rcf/set-timeout! 100)

(tests
  "async tests"
  #?(:clj  (tests
             (future
               (rcf/tap 1) (Thread/sleep 10)        ; tap value to queue
               (rcf/tap 2) (Thread/sleep 200)
               (rcf/tap 3))
             % := 1                               ; pop queue
             % := 2
             % := ::rcf/timeout)
     :cljs (tests
             (defn setTimeout [f ms] (js/setTimeout ms f))
             (rcf/tap 1) (setTimeout 10 (fn []
             (rcf/tap 2) (setTimeout 200 (fn []
             (rcf/tap 3)))))
             % := 1
             % := 2
             % := ::rcf/timeout))

  "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)

  "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))

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

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

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".

Can you improve this documentation? These fine people already did:
Dustin Getz, Geoffrey Gaillard, TristeFigure, xificurC & Eugen Stan
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close