clojure
htmx
筆記

Clojure TDD 測試

2026-01-20 22:05:5316min

在我side project 寫了幾個 clojure 的小專案後,一直感覺 repl 的工作流非常的快速。有了一個想法後,馬上可以透過 repl 驗證想法的正確性,讓開發者可以在很有信心的狀態下不斷的疊代新的功能。

然後,這樣的工作流我總覺得還少了一點什麼。

現在回頭看了一下一年多前寫的小專案,我想少了的東西應該就是自動化測試了。

為什麼要寫測試?

測試的最主要價值,不在於「檢查當下寫的程式的正確性」。我認為測試最重要的價值,在當一段時間過後,開發已經把當初開發的上下文、限制、需求細節等等忘記的差不多了的時候, 依然有個可以用來當作「文件」、「參考」、甚至「合約」的工具。

在接手別的人專案時,即使交接文件寫的有多麼的詳細、程式寫的有多麼乾淨、多麼模組化,當沒有測試的時候,沒有人敢隨意的改動程式碼。因為沒人知道,隨便的改動會不會破壞原本的行為。

即使你接手的人是「幾個月前的你」。

因此,我認為測試最主要的價值其實是「擬定合約」。工程師寫的測試,應該要在未來程式改動的時候,用來警告過去的某個「合約」被破壞了。有了這樣的信心後,後續工程師在改動時,才有辦法跟據合約, 更有信心的改動程式碼。

What is TDD

測試是測試軀動開發。強調測試先行,「先寫測試、後寫程式」的開發模式。教科書中的TDD常常被議論,是否為一個可行的開發模式。我是偏向支持測試軀動的益處多於害處的,但這篇不展開論敘。 在這篇文章內,想記錄的是,為了達成TDD,需要準備的方便測試的工具。

對我來說,測試先行在開發階段的優點,其實跟 repl 開發的優點一樣:可以快速的得到程式改動的回饋。因此一個良好回饋機制的工具非常重要。這樣的工具,應該達到的是:

  1. 響應檔案的變化並且可以重新執行程式測試
  2. 快速。

在我這次開發的過程中,主要找到兩種工具和模式可以達成這樣的需求。

TDD method in Clojure - cider auto testing

我是使用 emacs + cider + clojure lsp 做為開發 clojure 的主開要開發工具。cider 是一個功能豐富寫強大的工具。我在開發 clojure 時,第一步都是會先設定好 cider-jack-in 要使用的 alias。

之後,使用 cider-jack-in 打開 repl window 進入開發。

emacs cider screenshot

現在,假設要測試 greeting 這個函式。首先,可以在 函式名上使用 lsp-code-action,並且選 create test

我們使用 starter 產生的 greet 函式很簡單的印出一個 Hello World,因此我們寫簡單的測試函式測試它

(deftest greet-test
  (testing "should show hello, :name world successfully"
    (is (= "Hello, Howard World"  (greet "Howard")))))

寫完後,可使用 M-x cider-test-run-projcet-tests 跑專案的測試。另外可以打開 cider-auto-test-mode 開啟自動 rerun 相關測試。

測試 failed

可以看到,測試跑完,紅燈。

現在當試修這個bug。

clojure 的 println 回傳 nil。我們試著將字串回傳後重新執行。

(defn greet
  "Callable entry point to the application."
  [data]
  (let [msg (str "Hello, " (or (:name data) "World") "!")]
   (println msg)
   msg))

測試成功

可以看到,重新執行後(short cut: c-c c-k)測試成功。

這個workflow 整合了 emacs 的 cider repl flow,也可以做到 tdd 的快速測試。在小專案的規模已經很夠用了。

TDD method in clojure - kaocha test runner

lambdaisland 推出的 kaocha 簡介說明,是 clojure 下一世代的 test runner,其中包含 test coverage, watch mode, reports 等等功能, 並支援插件。它可以做為 clojure 測試的基礎工具。

同樣的,我們嘗試這個工具。在 deps.edn 內加入依賴

{:deps { ,,, }
 :aliases
 {:test {:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}
         :main-opts ["-m" "kaocha.runner"]}}}

kaocha 支援使用 tsets.edn 做為設定檔設定專案測試。照著 github 的提供的範例寫一個最簡單的設定檔。

#kaocha/v1
{:tests [{:id          :unit
          :test-paths  ["test" "src"]
          :ns-patterns [".*"]}]
          ;; :reporter kaocha.report.progress/report
          ;; :plugins [:kaocha.plugin/profiling :kaocha.plugin/notifier]
 }

不提供測試設定直接跑測試也是可以的,會使用預設的設定。

之後打開terminal 跑以下指令:

$ clj -M:test 
unit:   100% [==================================================] 2/2
2 tests, 2 assertions, 0 failures.

成功執行專案內測試。

kaocha 也支援 watch mode。試著將檔案回傳的內容改成錯誤訊息。

 clj -M:test --watch
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
unit:   100% [==================================================] 2/2
2 tests, 2 assertions, 0 failures.

[watch] Reloading #{com.howard.tdd-demo-test com.howard.tdd-demo}
unit:   100% [==================================================] 2/2

Randomized with --seed 772824609
FAIL in com.howard.tdd-demo-test/greet-test (tdd_demo_test.clj:11)
should show hello, :name world successfully
Expected:
  "Hello, Howard!"
Actual:
  -"Hello, Howard!" +"Hello, Howard"
╭───── Test output ───────────────────────────────────────────────────────
│ Hello, Howard
╰─────────────────────────────────────────────────────────────────────────
2 tests, 2 assertions, 1 failures.

在我的電腦上測試,回應速度非常快速。實際在真實專案內的響應速度以後有機會再測。

conclusion

稍為試過了 clojure 生態系的測試工具,我認為小但齊全。曾經看到有人說,repl driven 的開發沒有在寫測試。我認為兩者其實不相關。

repl driven 在開發時可以快速的驗證想法。但測試可以留作專案的「文件」或是「合約」。測試並沒有因此重要性減低。