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

4.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
Edit on GitHub

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

× close