ClojureScript testing with Figwheel.
Figwheel을 이용한 CLJS 테스팅.

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

Sadly, 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 figwheel.build]))
    (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]
    	    (wrap-CLJ-always-reload figwheel.build/figwheel-build)))
    (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)))