In ClojureScript, I wanted to run only tests changed, like as lein-test-refresh, but could not found appropriate package. So, I had implemented that functionality.

  1. Figwheel call a user function after reloading. You can register the function as follows:

    ;; project.clj
    {:cljsbuild {:build-id ... {:on-jsload some/on-reload}}}
    ;; some.clj
    (ns dev.tool
      (:require [cljs.test :as test]))
    (defn on-reload [reloaded-files]
      (->> reloaded-files
           (map :namespace)
           (map demunge)
           (map symbol)
           (apply test/run-tests namespaces) ;; <~~ run-tests is macro. :-<

    I thought I could easily implement the desired feature.

  2. Unfortunately, cljs.test is implemented as macros, we can not supply arguments to cljs.test/run-tests at runtime. We have to run tests that are related to the reloaed file, so we can not use cljs.test/run-tests function.
  3. I have dived into cljs.test, and I figured out that I can make the test as map:

    ;; CLJ
    (ns test.runner-macros
      (:require [cljs.analyzer.api :as ana.api]
                [cljs.test :as test]))
    (defmacro gen-ns-sym=>test-fn []
      `(def ~'ns-sym=>test-fn
         ~(->> (ana.api/all-ns)
               (filter #(re-find #"-test$" (name %)))
               (map (juxt #(identity `(quote ~%))
                          #(identity `(fn [env#] (test/test-ns-block env# (quote ~%))))))
               (into {}))))
    ;; CLJS
    (ns test.runner
      (:require-macros [test.runner-macros :refer [gen-ns-sym=>test-fn]])
      (:require [cljs.test :as test]))
    (defn run-tests
      [env-or-ns & namespaces]
      (let [env (if (map? env-or-ns)
            namespaces  (cond->> namespaces (symbol? env-or-ns) (cons env-or-ns))
            summary     (atom {:test 0 :pass 0 :fail 0 :error 0 :type :summary})
            accumulator #(swap! summary
                                (partial merge-with +)
                                (:report-counters (test/get-and-clear-env!)))
            reporter (fn []
                       (test/set-env! env)
                       (test/do-report @summary)
                       (test/report (assoc @summary :type :end-run-tests))
            test-fns (select-keys ns-sym=>test-fn namespaces)
            blocks   (concat (->> test-fns
                                  (map #(% env))
                                  (mapcat #(concat % [accumulator])))
        (if-not (empty? test-fns)
          (test/run-block blocks))))
  4. If the test changes, gen-ns-sym=>test-fn macro must be reloaded for changes to take effect. Figwheel always reload namespaces that have the :figwheel-always metadata. Unfortunately the :figwheel-always metadata not work in Clojure. But We can inject functions into the build process(middleware pattern) of Figwheel:

    (ns dev.repl
      (:require [figwheel-sidecar.repl-api :as figwheel]
                [figwheel-sidecar.config :as figwheel.config]
                [figwheel-sidecar.components.cljs-autobuild :as]))
    (def files
      ;; TODO
      ;; We should use tools.namespace.
      (->> ["tool/front/test/runner_macros.clj"]
           (map io/as-file)
           (filter #(.exists %))
           (map #(.getAbsolutePath %))))
    (defn wrap-CLJ-always-reload
      (fn [build-state]
        (build-fn (update build-state
                          :changed-files (partial concat files)))))
    (def config
      (assoc-in (figwheel.config/fetch-config)
                [:data :figwheel-options :cljs-build-fn]
    (figwheel/start-figwheel! config)
  5. Finally we can use run function instead of run-tests macro to run tests dynamically.

    (ns dev.tool
      (:require [test.runner :as test]))
    (defn on-reload [reloaded-files]
      (->> reloaded-files
           (map :namespace)
           (map demunge)
           (map symbol)
           (apply test/run namespaces)))