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:

  • Outline rendering

  • Left-to-right assertions

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

  • Real-time refresh of tests on save (client and server)

  • Seeing test results in any number of browsers at once

  • 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

  • Protocol testing support (helps prove network interactions are correct without running the full stack)

2. Client Tests

A dev-time only entry point for browser test rendering:

(ns cljs.user
  (:require
    [fulcro-spec.tests-to-run] (1)
    [fulcro-spec.suite :as suite]
    [fulcro-spec.selectors :as sel]))

(suite/def-test-suite on-load (2)
  {:ns-regex #"fulcro-spec\..*-spec"} (3)
  {:available #{:focused :should-fail} (4)
   :default #{::sel/none :focused}}) (5)
1tests_to_run.cljs Just requires all of your tests. This is necessary as all cljs test runners search by looking at the loaded namespaces. Since there are two places cljs tests can run from (browser and CI), it makes sense to keep this list in one file.
2Define a callback for figwheel to call to re run the tests.
3:ns-regex is the regex to filter the loaded namespaces down to just your tests.
4Is where you define your :available selectors, ie: legal/possible values for selectors on your specification.
5:default selectors to use when you haven’t yet specified any in the browser ui.

A cljsbuild in your project.clj (or if using boot: roughly) as follows:

{:source-paths ["src" "dev" "test"]
 :figwheel     {:on-jsload cljs.user/on-load} (1)
 :compiler     {:main          cljs.user (2)
                :output-to     "resources/public/js/test/test.js" (3)
                :output-dir    "resources/public/js/test/out"
                :asset-path    "js/test/out"
                :optimizations :none}}
1References the earlier defined test suite name so figwheel can re-run the tests when it reloads the js.
2Namespace entrypoint (eg: :main) points to the dev user namespace as described above.
3Must compile to a single javascript file at "resources/public/js/test/test.js".

An entrypoint for starting figwheel (or something equivalent if in boot):

(:require
  [com.stuartsierra.component :as cp]
  [figwheel-sidecar.system :as fsys])

(defn start-figwheel
  "Start Figwheel on the given builds, or defaults to build-ids in `figwheel-config`."
  ([]
   (let [props (System/getProperties) (1)
         figwheel-config (fsys/fetch-config)
         all-builds (->> figwheel-config :data :all-builds (mapv :id))]
     (start-figwheel (keys (select-keys props all-builds)))))
  ([build-ids]
   (let [figwheel-config (fsys/fetch-config)
         target-config (-> figwheel-config
                         (assoc-in [:data :build-ids]
                           (or (seq build-ids) (2)
                               (-> figwheel-config :data :build-ids))))] (2)
     (-> (cp/system-map
           :css-watcher (fsys/css-watcher {:watch-paths ["resources/public/css"]}) (3)
           :figwheel-system (fsys/figwheel-system target-config))
       cp/start :figwheel-system fsys/cljs-repl)))) (4)
1If no arguments, default to looking at the JVM system properties,
eg: if you have a build with id :test, it will look for -Dtest
2Otherwise it will use the passed in build-ids, or the default figwheel ids.
3OPTIONAL, starts a css watcher so you get automatic reloading of css when it changes.
4Starts a cljs repl you can interact with, see figwheel sidecar for more information

Open localhost:PORT/fulcro-spec-client-tests.html, where PORT is defined (with leiningen) by your figwheel configuration in your project.clj, eg: :figwheel {:server-port PORT}.

3. CI Tests

  • You should have a top level package.json file for installing the following:

{
  "devDependencies": {
    "karma": "^0.13.19",
    "karma-chrome-launcher": "^0.2.2",
    "karma-firefox-launcher": "^0.1.7",
    "karma-cljs-test": "^0.1.0"
  }
}
  • You have lein-doo as a plugin for running tests through karma via nodejs.

  • A all_tests.cljs file for running your tests.

(ns your.all-tests
  (:require
    fulcro-spec.tests-to-run (1)
    [doo.runner :refer-macros [doo-all-tests]]))

(doo-all-tests #"fulcro-spec\..*-spec") (2)
1For loading your test namespaces.
2Takes a regex to run just your test namespaces.
  • A :doo section to configure the CI runner. Chrome is the recommended target js-env.

:doo {:build "automated-tests", :paths {:karma "node_modules/karma/bin/karma"}}
  • A cljsbuild with id :automated-tests is the CI tests output.

{:source-paths ["src" "test"]
 :compiler     {:output-to     "resources/private/js/unit-tests.js"
                :main          fulcro-spec.all-tests
                :optimizations :none}}
  • An html file in your resources/private/, eg: unit-tests.html, for renderering your automated-tests build.

See lein-doo usage for up to date details on how to use it from the command line and how to setup the all_tests like file,
TLDR: lein doo ${js-env} automated-tests once, where for ex ${js-env} is chrome.

4. Server Tests

4.1. With reporting in the terminal

  • The lein test-refresh plugin, which will re-run server tests on save, and can be configured (see the :test-refresh section in the project.clj).

  • user.clj : The entry point for running clojure tests that should be rendered in the browser.

Use lein test-refresh at the command line. Read this changelog entry for information on using test-selectors.

A recommended sample configuration is:

:test-refresh {:report fulcro-spec.reporters.terminal/fulcro-report (1)
               :changes-only true (2)
               :with-repl true} (3)
1REQUIRED, :report must point to the correct report function.
2Only re-runs tests that have changed, useful if you have slow and/or many tests.
3Gives you a limited, but handy, repl!

For up to date and comprehensive information, you should treat lein-test-refresh itself as the authoritative source.

4.2. With reporting in the browser

  • Create a webserver using fulcro-spec.suite/def-test-suite that runs your tests and talks with your browser.
    See the docstring for further info on the arguments.

(:require
  [fulcro-spec.suite :as suite])

(suite/def-test-suite my-test-suite (1)
  {:config {:port 8888} (2)
   :test-paths ["test"] (3)(4)
   :source-paths ["src"]} (4)
  {:available #{:focused :unit :integration} (5)
   :default #{:fulcro-spec.selectors/none :focused :unit}}) (6)
1Defines a function you can call to (re-)start the test-suite, should be useful if you are changing the following arguments, ie: config, paths, or selectors.
2:config is passed directly to the webserver, only port is required and currently advertised as available.
3:test-paths is about finding your test namespaces.
4:source-paths is concatenated with :test-paths to create a set of paths that the test-suite will watch for any changes, and refresh and namespaces contain therein.
5Is where you define your :available selectors, ie: legal/possible values for selectors on your specification.
6:default selectors to use when you haven’t yet specified any in the browser ui.
  • Call my-test-suite and go to localhost:PORT/fulcro-spec-server-tests.html to view your test report.

5. Anatomy of a specification

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

(: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.

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

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

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

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

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

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