Liking cljdoc? Tell your friends :D

Fulcro Spec Docs

1. Features

The macros in fulcro-spec wrap clojure/cljs test, so that you may use any of the features of the core library. The specification DSL makes it much easier to read the tests, and also includes a number of useful features:

  • Left-to-right assertions

  • More readable output, such as data structure comparisons on failure (with diff notation as well)

  • Mocking of normal functions, including native javascript (but as expected: not macros or inline functions)

  • Mocking verifies call sequence and call count

  • Mocks can easily verify arguments received

  • Mocks can simulate timelines for CSP logic

2. Running Tests

You define the tests using deftest, so running them is normal. The recommended setup for full stack testing is as is done in the Fulcro Spec repository itself.

The following files are of interest:

src/test

The source of some tests (clj and cljs). The cljs cards use Workspaces cards for browser rendering.

deps.edn

The dependency and alias definitions. Note the kaocha setup.

tests.edn

Kaocha config, for running tests via tools.deps

.circleci/config.yml

A CI example

karma.conf.js

Config for Karma cljs runner

shadow-cljs.edn

Compile configs for building tests in workspaces

package.json

For CLJS deps.

Makefile

A sample UNIX make file for running the tests quickly from a command line with make.

3. Anatomy of a specification

The main testing macros are specification, behavior, component, and assertions:

specification is just an alias for deftest.

(:require
  [fulcro-spec.core :refer [specification behavior component assertions])

(specification "A Thing"
  (component "A Thing Part"
    (behavior "does something"
      (assertions
        form => expected-result
        form2 => expected-result2

        "optional sub behavior clause"
        form3 => expected-result3)))

See the clojure.spec.alpha/def for ::assertions in assertions.cljc for the grammar of the assertions macro.

component is an alias of behavior.
It can read better if you are describing a component [1] and not a behavior [2].

specification =outputs⇒ (clojure|cljs).test/deftest,
behavior =outputs⇒ (clojure|cljs).test/testing.

You are therefore free to use any functions from clojure.test or cljs.test inside their body.

However, we recommend you use these macros as opposed to deftest and testing as they emit extra reporting events that are used by our renderers.
You are however ok to use is instead of assertions if you prefer it.

3.1. Assertions

Assertions provides some explict arrows, unlike Midje which uses black magic, for use in making your tests more concise and readable.

(:require
  [fulcro-spec.core :refer [assertions])

(assertions
  actual => expected (1)
  actual =fn=> (fn [act] ... ok?) (2)
  actual =throws=> ExceptionType (3)(6)
  actual =throws=> #"message regex")
1Checks that actual is equal to expected, either can be anything.
2expected is a function takes actual and returns a truthy value.
3Expects that actual will throw an Exception and checks that the type is ExceptionType.

3.2. Mocking

The mocking system does a lot in a very small space. It can be invoked via the provided or when-mocking macro. The former requires a string and adds an outline section. The latter does not change the outline output. The idea with provided is that you are stating an assumption about some way other parts of the system are behaving for that test.

Mocking must be done in the context of a specification, and creates a scope for all sub-outlines. Generally you want to isolate mocking to a specific behavior:

(:require
  [fulcro-spec.core :refer [specification behavior when-mocking assertions]]
  [fulcro-spec.mocking :refer [calls-of])

;; source file
(defn my-function [x y] (launch-rockets!))
;; spec file
(specification "Thing"
  (behavior "Does something"
    (when-mocking
      (my-function arg1 arg2) => true
      ;;actual test
      (assertions
        (my-function 3 5) => true
        (calls-of my-function)
        => [{'arg1 3, 'arg2 5}]))))

Basically, you include triples (a form, arrow, form), followed by the code & tests to execute.

It is important to note that the mocking support does a bunch of verification at the end of your test:

  1. It uses the mocked functions in the order specified.

  2. It verifies that your functions are called the appropriate number of times (at least once is the default) and no more if a number is specified.

  3. It captures the arguments in the symbols you provide (in this case arg1 and arg2). These are available for use in the RHS of the mock expression.

  4. If the mocked function has a clojure.spec.alpha/fdef with :args, it will validate the arguments with it.

  5. It returns whatever the RHS of the mock expression indicates.

  6. If the mocked function has a clojure.spec.alpha/fdef with :ret, it will validate the return value with it.

  7. If the mocked function has a clojure.spec.alpha/fdef with :fn (and :args & :ret), it will validate the arguments and return value with it.

  8. It provides a way to access the arguments and return values of the mocked functions (in fulcro-spec.mocking), that you can use to make your own assertions.

So, the following mock script should pass:

(:require
  [fulcro-spec.core :refer [when-mocking assertions])

(when-mocking
  (f a) =1x=> a (1)
  (f a) =2x=> (+ 1 a) (2)
  (g a b) => 17 (3)

  (assertions
    (+ (f 2) (f 2) (f 2)
       (g 3e6 :foo/bar)
       (g "otherwise" :invalid)) (4)
    => 42))
1The first call to f returns the argument.
2The next two calls return the argument plus one.
3g can be called any amount (but at least once) and returns 17 each time.
4If you were to remove any call to f or g this test would fail.

3.2.1. fulcro-spec.mocking API

The namespace fulcro-spec.mocking provides helper functions to access the arguments and return values of a mocked function. Is the new preferred method to make assertions about what a mocked function received and returned.

(:require
  [fulcro-spec.core :refer [when-mocking assertions]]
  [fulcro-spec.mocking :as mock])

(defn f [a] (inc a))

(when-mocking
  (f a1) =1x=> (mock/real-return)
  (f a*) => a
  (assertions
    (f 0) => 1
    (f 5) => 5
    (mock/calls-of f)
    => [{'a1 0} (1)
        {'a* 5}]
    (mock/call-of f 0) (2)
    => {'a1 0}
    (mock/call-of f 3) (3)
    => nil
    (mock/spied-value f 1 'a*)
    => 5
    (mock/returns-of f)
    => [1 5]
    (mock/return-of f 0)
    => 1))
1Note that the symbols returned match what was specified in the mock definition, not the defn.
2Note that the index is a 0 based (is passed to nth).
3Note that all these functions return nil if the function was not mocked, the index was not found, or the requested symbol was not found.

3.2.2. Clojure.spec mocking integration

However, the following mock script will fail due to clojure.spec.alpha errors:

(:require
  [clojure.spec.alpha :as s]
  [fulcro-spec.core :refer [when-mocking assertions])

(s/fdef f
  :args number?
  :ret number?
  :fn #(< (:args %) (:ret %)))
(defn f [a] (+ a 42))

(when-mocking
  (f "asdf") =1x=> 123 (1)
  (f a) =1x=> :fdsa (2)
  (f a) =1x=> (- 1 a) (3)

  (assertions
    (+ (f "asdf") (f 1) (f 2)) => 42))
1Fails the :args spec number?
2Fails the :ret spec number?
3Fails the :fn spec (< args ret)

3.2.3. Spies

Sometimes it is desirable to check that a function is called but still use its original definition, this pattern is called a test spy. Here’s an example of how to do that with fulcro spec:

(:require
  [fulcro-spec.core :refer [when-mocking assertions]]
  [fulcro-spec.mocking :refer [real-return]])

(specification "..."
  (when-mocking f => (real-return)
  (assertions
    ...)

3.2.4. Protocols and Inline functions

When working with protocols and records, or inline functions (eg: +), it is useful to be able to mock them just as a regular function. The fix for doing so is quite straightforward:

;; source file
(defprotocol MockMe
  (-please [this f x] ...)) (1)
(defn please [this f x] (-please this f x)) (2)

(defn fn-under-test [this]
  ... (please this inc :counter) ...) (3)

;; test file
(:require
  [fulcro-spec.core :refer [when-mocking assertions])

(when-mocking
  (please this f x) => (do ...) (4)
  (assertions
    (fn-under-test ...) => ...))) (5)
1define the protocol & method
2define a function that just calls the protocol
3use the wrapper function instead of the protocol
4mock the wrapping function from (2)
5keep calm and carry on testing

3.3. Timeline testing

On occasion you’d like to mock things that use callbacks. Chains of callbacks can be a challenge to test, especially when you’re trying to simulate timing issues.

(:require
  [cljs.test :refer [is]]
  [fulcro-spec.core :refer [specification provided with-timeline
                               tick async]])

(def a (atom 0))

(specification "Some Thing"
  (with-timeline
    (provided "things happen in order"
              (js/setTimeout f tm) =2x=> (async tm (f))

              (js/setTimeout
                (fn []
                  (reset! a 1)
                  (js/setTimeout
                    (fn [] (reset! a 2)) 200)) 100)

              (tick 100)
              (is (= 1 @a))

              (tick 100)
              (is (= 1 @a))

              (tick 100)
              (is (= 2 @a))))

In the above scripted test the provided (when-mocking with a label) is used to mock out js/setTimeout. By wrapping that provided in a with-timeline we gain the ability to use the async and tick macros (which must be pulled in as macros in the namespace). The former can be used on the RHS of a mock to indicate that the actual behavior should happen some number of milliseconds in the simulated future.

So, this test says that when setTimeout is called we should simulate waiting however long that call requested, then we should run the captured function. Note that the async macro doesn’t take a symbol to run, it instead wants you to supply a full form to run (so you can add in arguments, etc).

Next this test does a nested setTimeout! This is perfectly fine. Calling the tick function advances the simulated clock. So, you can see we can watch the atom change over \"time\"!

Note that you can schedule multiple things, and still return a value from the mock!

(:require
  [fulcro-spec.core :refer [provided with-timeline async]])

(with-timeline
  (when-mocking
     (f a) => (do (async 200 (g)) (async 300 (h)) true)))

the above indicates that when f is called it will schedule (g) to run 200ms from \"now\" and (h) to run 300ms from \"now\". Then f will return true.

3.4. Check(ing)

The fulcro-spec.check exposes functions to help make more useful and precise assertions in your tests. Here is a sampling of what is possible:

(:require
  [fulcro-spec.core :refer [assertions]]
  [fulcro-spec.check :as _])

(assertions
  "equals?*"
  1 =check=> (_/equals?* 1)

  "is?*"
  2 =check=> (_/is?* int?)

  "valid?* - uses clojure.spec.alpha"
  3 =check=> (_/valid?* int?)

  "re-find?*"
  "4" =check=> (_/re-find?* #"\d")

  "seq-matches-exactly?*"
  [5] =check=> (_/seq-matches-exactly?* [5])
  [5 6] =check=> (_/seq-matches-exactly?*
                   [(_/is?* int?) (_/equals?* 6)])

  "every?*"
  [7 8] =check=> (_/every?* (_/is?* int?))

  "embeds?*"
  {:a 9, :b 10}
  =check=> (_/embeds?* {:a 9, :b (_/is?* int?)})

  "throwable*"
  (throw (ex-info "" {}))
  =throws=> (_/throwable* (_/is?* some?))

  "ex-data*"
  (throw (ex-info "" {:c 11}))
  =throws=> (_/ex-data* (_/equals?* {:c 11})))

You can make your own checkers using the _/checker macro. They are simply functions that are expected to conditionally return maps each representing a failed assertion.

(defn my-equals?* [expected]
  (_/checker [actual]
    (when-not (= actual expected) (1)
      {:actual actual (2) (3) (4)
       :expected expected
       :message "my-equals?* failed!"})))

((my-equals?* 55) 33)
;=> {:actual 33 :expected 55 ,,,}
1nil or an empty sequence is considered passing.
2A checker can return a single failure, or many (arbitrarily nested, as will be flatten -ed by the =check⇒ arrow).
3Note that for a map to be considered a failure, it must contain one of the following keys #{:actual :expected :message :type}.
4The shown map keys are what clojure.test understands, but it is an open map that you can extend. When in an assertions macro all failures will sent to the current clojure.test reporter, but there are no guarantees that it will be understood or reported by it.

3.4.1. Advanced usage

To combine multiple checkers into a single assertion, two functions are provided. The first is all*, and it will run all it’s checkers. The second is and*, it will short circuit execution on the first failure. Note that because checkers can return multiple failures, it is not guaranteed that and* will return only a single failure.

(assertions
  ((all* (_/equals?* 1) (_/is?* int?)) 5.0)
  => [{:actual 5.0 :expected 1}
      {:actual 5.0 :expected int?}]

  ((and* (_/is?* int?) (_/equals?* 1)) 5.0)
  => {:actual 5.0 :expected int?})

It can be useful to run a function on a value before passed to a checker, such as sorting. For this you can use fmap*, but use it judiciously, as you can perform arbitrary transformations that may make your test failures harder to understand.

(assertions
  [:c :a :b] => (_/fmap* sort (_/equals [:a :b :c])))

4. Transitive Coverage Proofs

The coverage analysis system enables building provable chains of test coverage from low-level functions up to application logic. This is a Clojure-only feature that requires guardrails (>defn) for call graph analysis.

4.1. Overview

The system tracks which tests cover which functions, and can verify that a function and all its transitive dependencies have test coverage. Additionally, it supports staleness detection: when a function’s source code changes after a test was sealed, the coverage is marked as stale until the developer reviews and re-seals the test.

Key namespaces:

  • fulcro-spec.coverage - Registry tracking which tests cover which functions

  • fulcro-spec.signature - Computes signatures for staleness detection

  • fulcro-spec.proof - High-level API for coverage verification

4.2. Declaring Coverage

Tests declare which functions they cover via the :covers metadata on specification:

(:require
  [fulcro-spec.core :refer [specification behavior assertions]]
  [fulcro-spec.proof :as proof])

;; With signature (enables staleness detection)
(specification {:covers {`my-fn "abc123"}} "my-fn test"
  (assertions
    (my-fn 1 2) => 3))

;; Legacy format (no staleness detection)
(specification {:covers [`my-fn]} "my-fn test"
  (assertions
    (my-fn 1 2) => 3))

The signature is a 6-character hash of the function’s normalized source code (with docstrings removed and whitespace normalized). This means:

  • Changing implementation DOES change the signature

  • Changing docstrings does NOT change the signature

  • Reformatting whitespace does NOT change the signature

4.3. Getting Signatures

Use signature to get the signature for a function. The signature format is automatically determined based on the function’s call graph:

(require '[fulcro-spec.signature :as sig])
(require '[fulcro-spec.proof :as proof])

;; With .fulcro-spec.edn configured, just pass the function symbol:
(sig/signature 'myapp.core/my-function)
(proof/signature 'myapp.core/my-function)
;; => "a1b2c3" (leaf function - no in-scope callees)
;; OR
;; => "a1b2c3,def456" (non-leaf function - has callees)

;; Or with explicit scope:
(sig/signature 'myapp.core/my-function #{"myapp"})

Signature formats:

  • LEAF functions (no in-scope callees): "xxxxxx" - just the 6-char hash of the function’s own source

  • NON-LEAF functions (has callees): "xxxxxx,yyyyyy" - the function’s hash plus a hash of all transitive callees' signatures

This unified approach ensures:

  • Leaf functions get simple signatures (no redundant suffix)

  • Non-leaf functions automatically track their entire call tree

  • Changes to any function in the call chain invalidate dependent tests

4.4. Configuring Scope

Before using the coverage system, configure which namespaces are "in scope" for coverage checking. The simplest approach is to create a .fulcro-spec.edn file in your project root:

;; .fulcro-spec.edn
{:scope-ns-prefixes #{"myapp" "myapp.lib"}}

This file is automatically loaded when coverage features are first used. Alternatively, configure programmatically:

(proof/configure! {:scope-ns-prefixes #{"myapp" "myapp.lib"}
                   :enforce? false})

Options:

  • :scope-ns-prefixes - Set of namespace prefix strings that define which namespaces are in scope. E.g., #{"myapp"} includes myapp.core, myapp.db, etc.

  • :enforce? - When true, when-mocking!! and provided!! will throw if mocked functions lack transitive coverage. Default: false.

4.5. Querying Coverage

4.5.1. Simple Queries

;; Is this function and all its transitive deps covered?
(proof/fully-tested? 'myapp.orders/create-order)
;; => true or false

;; What's missing?
(proof/why-not-tested? 'myapp.orders/create-order)
;; => {:uncovered #{myapp.db/save!} :stale #{myapp.validation/check}}
;; => nil if fully tested

4.5.2. Detailed Report

(proof/verify-transitive-coverage 'myapp.orders/create-order)
;; => {:function myapp.orders/create-order
;;     :scope-ns-prefixes #{"myapp"}
;;     :transitive-deps #{...}
;;     :covered #{...}
;;     :uncovered #{...}
;;     :stale #{...}
;;     :proof-complete? true/false}

;; Human-readable output
(proof/print-coverage-report 'myapp.orders/create-order)

4.5.3. Scope-wide Statistics

(proof/coverage-stats)
;; => {:total 42
;;     :covered 38
;;     :uncovered 4
;;     :stale 2
;;     :fresh 36
;;     :coverage-pct 90.47619047619048}

;; All uncovered functions
(proof/uncovered-in-scope)
;; => #{myapp.db/save! myapp.cache/invalidate!}

4.6. Staleness Detection

When you seal a test with a signature and later modify the function’s implementation, the coverage becomes "stale". This helps ensure tests are reviewed when code changes.

4.6.1. Checking Staleness

;; Has this function been sealed with a signature?
(proof/sealed? 'myapp.core/my-fn)
;; => true/false

;; Is the sealed signature current?
(proof/fresh? 'myapp.core/my-fn)
;; => true only if sealed AND signature matches current

;; Has the implementation changed since sealing?
(proof/stale? 'myapp.core/my-fn)
;; => true if sealed AND signature differs from current

The fresh? and stale? functions work with both leaf functions (single-field signatures) and non-leaf functions (two-field signatures). They automatically compare the sealed signature against the current computed signature.

4.6.2. Finding Stale Functions

;; All stale functions in scope
(proof/stale-functions)
;; => #{myapp.core/changed-fn myapp.db/modified-fn}

;; Detailed stale coverage info
(proof/stale-coverage)
;; => {myapp.core/changed-fn {:sealed-sig "abc123"
;;                            :current-sig "def456"
;;                            :tested-by #{myapp.core-test/my-test}}}

4.6.3. Re-sealing Workflow

When functions become stale, use reseal-advice to get the new signatures:

;; Get advice for stale functions
(proof/reseal-advice)
;; => {myapp.core/changed-fn {:new-signature "def456,abc789"
;;                            :tested-by #{myapp.core-test/my-test}}}

;; Human-readable advice
(proof/print-reseal-advice)
;; Stale functions needing re-seal:
;; ================================
;;   myapp.core/changed-fn
;;     New signature: def456,abc789
;;     Tested by: myapp.core-test/my-test
;;     Update to: {:covers {`changed-fn "def456,abc789"}}

Then update your test’s :covers declaration with the new signature. The signature format (single-field for leaves, two-field for non-leaves) is determined automatically.

4.7. Enforcement with Double-Bang Mocking

The when-mocking!! and provided!! macros (note double-bang) enforce transitive coverage for mocked functions:

(proof/configure! {:scope-ns-prefixes #{"myapp"}
                   :enforce? true})

(specification "High-level test"
  ;; This will throw if helper-fn lacks transitive coverage
  (when-mocking!!
    (helper-fn x) => :mocked

    (assertions
      (main-fn 1) => :expected)))

When enforcement is enabled and a mocked function (or its transitive dependencies) lacks coverage or has stale coverage, an exception is thrown with details about what’s missing.

Single-bang versions (when-mocking! / provided!) do not enforce coverage.

4.8. Baseline Export/Import

For CI integration or comparing coverage over time:

;; Export current signatures as a baseline
(def baseline (proof/export-baseline))
;; => {:generated-at #inst "..."
;;     :scope-ns-prefixes #{"myapp"}
;;     :signatures {myapp.core/fn1 "abc123" ...}}

;; Save to file
(spit ".fulcro-spec-baseline.edn" (pr-str baseline))

;; Later, compare against baseline
(def old-baseline (read-string (slurp ".fulcro-spec-baseline.edn")))
(proof/compare-to-baseline old-baseline)
;; => {:changed #{...}    - functions whose signatures differ
;;     :added #{...}      - new functions not in baseline
;;     :removed #{...}    - functions no longer in scope
;;     :unchanged #{...}} - functions with matching signatures

4.9. Auto-Skip for Unchanged Tests

When running full test suites, you can enable automatic skipping of tests whose covered functions have not changed since they were last sealed. This provides fast feedback when re-running tests after small changes.

4.9.1. Java System Properties

Two properties control auto-skip behavior (presence of property enables feature):

PropertyDescription

fulcro-spec.auto-skip

When set, tests with matching signatures are skipped

fulcro-spec.sigcache

When set, signatures are cached for the JVM lifetime (for full suite runs)

4.9.2. Usage Scenarios

Interactive development (default):

# No properties set - all tests run, signatures computed fresh each time
clojure -M:test

Fast local iteration with auto-skip:

# Skip unchanged tests, but still recompute signatures (catches edits)
clojure -J-Dfulcro-spec.auto-skip -M:test

Full suite run with maximum performance:

# Skip unchanged AND cache signatures (fastest, but restart JVM after edits)
clojure -J-Dfulcro-spec.auto-skip -J-Dfulcro-spec.sigcache -M:test

4.9.3. Configuration

The coverage system requires configuration to know which namespace prefixes are in scope. Configuration is handled automatically via a .fulcro-spec.edn file in your project root:

;; .fulcro-spec.edn
{:scope-ns-prefixes #{"myapp" "myapp.lib"}}

The system automatically loads this file when coverage features are first used. If the file is missing, a warning is printed and coverage features are disabled:

WARNING: .fulcro-spec.edn not found. Coverage features disabled.
         Create .fulcro-spec.edn with {:scope-ns-prefixes #{"your.ns.prefix"}}

Configuration options:

OptionDescription

:scope-ns-prefixes

Set of namespace prefix strings that define which namespaces are in scope for coverage analysis. E.g., #{"myapp"} includes myapp.core, myapp.db, etc.

:enforce?

When true, when-mocking!! and provided!! throw if mocked functions lack transitive coverage. Default: false.

Programmatic override:

You can also configure programmatically, which takes precedence over the file:

(require '[fulcro-spec.proof :as proof])
(proof/configure! {:scope-ns-prefixes #{"myapp"}
                   :enforce? true})

Without configuration (no file and no configure! call), coverage features are disabled and tests run normally.

4.9.4. How It Works

When a test has :covers metadata with signatures:

(specification {:covers {`process-order "a1b2c3,d4e5f6"}} "order processing" ...)

At runtime, if auto-skip is enabled:

  1. The current signature is computed for each covered function

  2. If ALL signatures match their sealed values, the test body is replaced with a simple (is true) assertion

  3. The test output shows: "skipped. Unchanged since last run"

This means:

  • Tests with no :covers metadata always run

  • Tests with stale signatures always run

  • Only tests with all matching signatures are skipped

4.9.5. Zero Overhead When Disabled

The system is designed for zero overhead when disabled:

  • Property checks use delay - evaluated once, cached forever

  • already-checked? short-circuits immediately when auto-skip is false

  • No signature computation occurs unless auto-skip is enabled

  • No call graph analysis happens unless needed

This ensures REPL interactions remain fast when the feature is not in use.

4.10. Limitations

  • Clojure only - Call graph analysis uses guardrails which is not available in ClojureScript

  • Requires guardrails - Functions must be defined with >defn for call graph tracking

  • Source must be available - Signatures cannot be computed for compiled-only code or REPL definitions without files

5. REPL Usage (Clojure)

The terminal reporter is quite easy and useful to use at the REPL for Clojure. Simply start a REPL, and define a keyboard shortcut to run something like this:

;; make sure you put the REPL in the (possibly reloaded) ns (your REPL might be closing over an old version)
(in-ns (.getName *ns*))
;; Make sure the REPL runner is loaded
(require 'fulcro-spec.reporters.repl)
;; Run all tests in current ns
(fulcro-spec.reporters.repl/run-tests)

5.1. Controlling Stack Trace Output

There are two dynamic vars that can be used to filter/limit stack traces in the outline output:

fulcro-spec.reporters.terminal/exclude-files

What (simple) filenames to ignore stack frames from. (default #{"core.clj" "stub.cljc"})

fulcro-spec.reporters.terminal/stack-frames

How many non-filtered frames to print (default 10)

Use alter-var-root to reset the value of these globally.


1. Noun: a part or element of a larger whole. Adjective: constituting part of a larger whole; constituent.
2. Noun: the way in which a natural phenomenon or a machine works or functions.

Can you improve this documentation? These fine people already did:
Anthony D'Ambrosio, Tony Kay & Miguel SM
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close