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=> (ExceptionType opt-regex opt-pred) (4)(6)
  actual =throws=> {:ex-type opt-ex-type :regex opt-regex :fn opt-pred}) (5)(6)
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.
4Can also optionally that the message matches the opt-regex & opt-pred.
5An alternative supported syntax is a map with all optional keys :ex-type :regex :fn
6View the clojure.spec.alpha/def ::criteria assertions.cljc for the up to date grammar for the expected side of a =throws⇒ assertions.

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

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

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. If assertions run in the RHS form, they will be honored (for test failures).

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

(let [real-fn f]
  (when-mocking f => (do ... (real-fn))
  (assertions
    ...)

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


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?Edit on GitHub

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

× close