你是否有過這樣的經驗:本來只是修改了 A 部分的程式,結果改完之後 B 部分的程式竟然不正常;或者是以前早就改好的問題,在這次改版之後又出現了呢?當有這些情形出現,你需要的是透過測試來提早檢查可能出錯的地方,並找出會導致出錯的條件。
除了人工測試之外,最應該加入的就是有一組編寫好的測試程式。有了這些測試程式,你可以在正式上線或提交程式碼之前,利用這些測試程式檢查新修改的程式是否符合規定,也可以保證過往的問題不再出現。
本文就跟大家介紹在 Clojure 專案中撰寫測試的方法。使用的工具是內建的 clojure.test
。
使用 clojure.test
之前,要記得在測試程式中將 clojure.test
的命名空間引入,如下所示:
(ns clj-test.core-test
(:require [clojure.test :refer :all]))
或在 REPL 中載入 clojure.test
命名空間:
(require '[clojure.test :refer :all])
如果使用 leiningen 建立專案,專案中已經有一個 test 目錄,其中有自動產生的空白測試程式碼範本,把測試程式寫在那裏就對了。
像其他的測試框架一樣,clojure.test
提供了斷言 (Assertion) 用來判斷一段程式的結果是否符合預期。它提供了 is
這個巨集判斷結果是否爲真,以下是它的使用範例:
(is (= 4 (+ 2 2)))
;; => true
(is (.startsWith "abcde" "ab"))
;; => true
(is (instance? Integer 256))
;; => FAIL in () (form-init542440103815122923.clj:1)
;; => expected: (instance? Integer 256)
;; => actual: java.lang.Long
;; => false
除了 is
之外,clojure.test
還提供了 are
這個巨集,幫助你將許多相似的判斷整理起來,使用方法如下:
(are [x y] (= x y)
4 (+ 2 2)
2 (+ 1 1))
;; => true
上面的範例跟以下的範例是一樣的,但是透過 are
就可以彙整衆多的 is
程式:
(is (= 4 (+ 2 2)))
(is (= 2 (+ 1 1)))
因爲 Clojure 是一個依靠 JVM 的語言,除了測試要執行的程式是否符合預期,也會有需要測試是否出現例外的時候。提供了 thrown?
巨集來完成這項工作:
(is (thrown? ArithmeticException (/ 1 0)))
測試框架 clojure.test
提供 deftest
給使用者定義自己的測試案例,透過 deftest
中一個個寫好的判斷程式,來檢查欲執行的程式是否正確。
(deftest addition
(is (= 4 (+ 2 2)))
(is (= 7 (+ 3 4))))
不同的測試案例也可以再由另一個 deftest
包覆起來成爲一個更高階的測試案例。
(deftest arithmetic
(addition)
(subtraction))
要清楚講明測試案例的作用,除了把命名儘量寫的容易理解之外,另一個方式就是在測試案例的說明註解中寫清楚。在 clojure.test
中,想利用文字清楚說明測試的意圖,可以在 is
中加上說明文字。當測試出錯時,該處的文字會出現在錯誤報告中:
(is (= 5 (+ 2 2)) "Crazy arithmetic")
;; => FAIL in () (form-init542440103815122923.clj:1)
;; => Crazy arithmetic
;; => expected: (= 5 (+ 2 2))
;; => actual: (not (= 5 4))
;; => false
另外也提供了 testing
巨集,讓撰寫測試者可以將幾個斷言聚集在一起加上說明文字,不至於散亂而更有組織。同樣地,說明文字也會出現在錯誤報告中:
(deftest arithemetic-test
(testing "Arithmetic"
(testing "with positive integers"
(is (= 4 (+ 2 2)))
(is (= 7 (+ 3 4))))
(testing "with negative integers"
(is (= -4 (+ -2 -2)))
(is (= -1 (+ 3 -4))))))
要注意的是,testing
巨集只能在 deftest
中使用。
寫好測試案例之後,可以透過 clojure.test
提供的 run-tests
來執行寫好的測試範例:
(run-tests 'your.namespace 'some.other.namespace)
如果在 run-tests
中沒有寫下命名空間,將會執行目前命名空間中的測試案例。
(run-tests)
;; =>
;; => Testing user
;; =>
;; => Ran 2 tests containing 6 assertions.
;; => 0 failures, 0 errors.
;; => {:test 2, :pass 6, :fail 0, :error 0, :type :summary}
或在命令列下使用 leiningen 執行測試:
$ lein test
lein test clj-test.core-test
lein test :only clj-test.core-test/a-test
FAIL in (a-test) (core_test.clj:7)
FIXME, I fail.
expected: (= 0 1)
actual: (not (= 0 1))
Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
Tests failed.
有時候一些相關的測試案例執行之前,需要先啓動某些資源。比如資料庫的測試案例,就必須在所有測試開始之前,先與資料庫做好連線。在測試完畢之後,妥善地恢復成之前的樣貌。這種在測試案例之中的環境準備,就稱爲治具 (Fixture)。
在 clojure.test
中,Fixture 只是一個簡單的函式,它只接受一個參數。這個參數就是待執行的測試案例,如果想要在執行測試案例前後做一些準備或善後作業,只要在測試案例前後執行即可,範例如下:
(defn my-fixture [test-fn]
;; 在這裡設定或啓動必須事先準備好的事物
(test-fn) ;; 呼叫測試案例
;; 在這裡做善後工作
)
Fixture 分爲兩種,一種是只需要執行一次,另一種是針對每個測試案例都會執行一次。以下是使用範例:
(use-fixtures :once load-data-fixture) ;; 只執行一次
(use-fixtures :each add-test-id-fixture) ;; 每個測試案例都會執行一次
經由本篇文章,你知道了如何引入 clojure.test
命名空間開始進行測試,也知道了幾種斷言可以用來檢驗運算式是否正確。知道了撰寫測試案例的方式,還有用說明文字輔助解釋測試案例。還知道了如何執行測試案例以及治具的使用方法。
還不賴吧?今天就先到這裡,下一篇文章再見囉!
(本篇文章同步刊登於 GitHub,歡迎在文章下方留言或發送 PR 給予建議與指教)