Clojure 测试:clojure.test 库
安装 clojure.test 是 Clojure 标准库的一部分,无需额外安装。只需要在项目中引入即可开始编写测试。
创建 测试文件。通常测试文件与源代码文件一一对应,放在 test 目录下。例如,如果源代码文件是 src/myproject/core.clj,则测试文件应为 test/myproject/core_test.clj。
基础测试结构
使用 clojure.test 命名空间来开始编写测试。
(ns myproject.core-test
(:require [clojure.test :refer :all]
[myproject.core :as core]))
编写 第一个测试使用 deftest 宏:
(deftest test-addition
(is (= 4 (core/add 2 2))))
运行 测试,使用以下命令:
lein test
或
clj -M:test
断言类型
使用 is 宏创建简单断言:
(deftest test-basic-assertions
(is true)
(is (= 4 (+ 2 2)))
(is (not= 5 (+ 2 2)))
(is (nil? nil))
(is (throw? (IllegalStateException. "error"))))
测试 异常使用 is 与 throw? 结合:
(deftest test-exception
(is (thrown? ArithmeticException (/ 1 0))))
使用 are 宏进行参数化测试:
(deftest test-multiplication
(are [x y result] (= result (* x y))
2 2 4
3 3 9
4 5 20))
测试 条件逻辑使用 testing 宏:
(deftest test-complex-logic
(testing "当输入为正数时"
(is (= 1 (core/abs 1)))
(is (= 1 (core/abs -1))))
(testing "当输入为零时"
(is (= 0 (core/abs 0)))))
测试夹具(Fixtures)
使用 use-fixtures 设置测试前后的操作:
(defn clean-up
[f]
(println "准备测试环境...")
(f)
(println "清理测试环境..."))
(use-fixtures :each clean-up)
创建 仅在测试套件开始和结束时运行的夹具:
(defn setup-test-suite
[f]
(println "设置测试套件...")
(try
(f)
(finally
(println "清理测试套件..."))))
(use-fixtures :once setup-test-suite)
使用 命名空间级别的夹具:
(defn before-all
[f]
(println "所有测试前运行")
(f))
(defn after-all
[f]
(try
(f)
(finally
(println "所有测试后运行"))))
(use-fixtures :once before-all after-all)
测试组织和运行
使用 test-ns 运行当前命名空间的所有测试:
(test-ns 'myproject.core-test)
运行 特定测试:
(run-tests 'myproject.core-test)
运行 多个命名空间测试:
(run-tests 'myproject.core-test 'myproject.utils-test)
过滤 运行特定测试:
(run-tests #"test-addition") ; 运行名称匹配正则表达式的测试
测试结果和报告
获取 测试结果详情:
(defn run-with-details
[]
(let [results (run-tests 'myproject.core-test)]
(println "通过:" (:pass results))
(println "失败:" (:fail results))
(println "错误:" (:error results))))
使用 测试报告功能:
(use-fixtures :once
(fn [f]
(println "=== 开始测试 ===")
(f)
(println "=== 测试结束 ===")))
测试标记和分类
添加 元数据到测试:
(deftest ^:slow ^:integration test-slow-operation
(is (= "result" (core/slow-operation))))
过滤 运行带有特定标记的测试:
;; 在 test_select.clj 中定义测试选择器
(defmethod clojure.test/report [ :end-test-suite] [m]
(when (some #(-> % :meta (get :slow)) (:test-var m))
(println "发现慢速测试")))
测试的最佳实践
确保 测试的独立性,每个测试不应依赖于其他测试的执行顺序。
避免 在测试中使用 I/O 操作,尽量模拟或使用测试替身(test doubles)。
保持 测试简洁明了,一个测试应只测试一个功能点。
使用 描述性名称,测试名称应清晰表达被测试的行为。
定期 运行测试,确保代码更改不会破坏现有功能。
覆盖 边界条件和异常情况,而不仅仅是"happy path"。
验证 错误处理和边界条件,确保程序在各种情况下都能优雅地处理错误。
重构 测试代码以保持清晰和可维护性,就像重构生产代码一样。
使用 测试覆盖率工具来确保测试覆盖了足够的代码路径:
lein cloverage
clojure.test 常用函数和宏
以下是 clojure.test 中最常用的函数和宏:
| 函数/宏 | 描述 | 示例 |
|---|---|---|
deftest |
定义测试 | (deftest test-name (is (= 1 1))) |
is |
创建断言 | (is (= 1 (+ 1 0))) |
are |
参数化测试 | (are [x y] (= y x) 1 1 2 2) |
testing |
创建测试组 | (testing "描述" (is condition)) |
run-tests |
运行测试 | (run-tests 'namespace) |
test-var |
运行特定测试 | (test-var test-var-symbol) |
fixture |
设置测试夹具 | (use-fixtures :each setup-teardown) |
report |
自定义报告 | (defmethod clojure.test/report [ :end-test] [m] ...) |
扩展 测试报告,可以添加自定义报告方法:
(defmethod clojure.test/report [:begin-test-var] [m]
(println (str "开始测试: " (-> m :var :name))))
(defmethod clojure.test/report [:pass] [m]
(println (str "✓ 通过: " (-> m :test-var :name))))
编写 自定义断言助手函数:
(defn is-equal-vectors
"测试两个向量是否包含相同的元素,顺序不重要"
[expected actual]
(is (= (set expected) (set actual))))
(deftest test-vector-equality
(is-equal-vectors [1 2 3] [3 2 1]))
集成测试策略
组织 集成测试与单元测试分离,使用不同的命名空间或目录:
;; 在 test/myproject/integration_test.clj 中
(ns myproject.integration-test
(:require [clojure.test :refer :all]
[myproject.core :as core]
[myproject.db :as db]))
(defn setup-db
[f]
(db/initialize-test-db)
(f)
(db/cleanup-test-db))
(use-fixtures :once setup-db)
(deftest test-full-workflow
(is (= "创建成功" (core/create-item {:name "测试项"})))
(is (= 1 (count (core/get-all-items)))))
模拟 外部依赖,测试系统集成:
(ns myproject.external-api-test
(:require [clojure.test :refer :all]
[clojure.data.json :as json]))
(def mock-api-client
(reify
myproject.api/ApiClient
(get-data [this id]
{:status 200 :body (json/write-str {:id id :name "模拟数据"})})))
(deftest test-api-integration
(with-redefs [myproject.api/client mock-api-client]
(let [response (myproject.api/get-user-data 123)]
(is (= 200 (:status response)))
(is (= "模拟数据" (-> response :body json/read-str :name))))))
性能测试
使用 clojure.test 的 time 宏进行简单性能测试:
(deftest test-performance
(time
(dotimes [_ 1000]
(core/heavy-computation))))
实现 自定义性能测试框架:
(defmacro benchmark
"运行代码多次并返回平均执行时间"
[n & body]
`(let [start# (System/currentTimeMillis)
results# (doall (repeatedly ~n (fn [] ~body)))
end# (System/currentTimeMillis)
avg# (/ (- end# start#) ~n)]
(println "平均执行时间:" avg# "ms")
results#))
(deftest test-benchmark
(benchmark 100 (core/heavy-computation)))
测试驱动开发(TDD)在 Clojure 中的应用
遵循 红色-绿色-重构的循环:
- 红色:编写一个失败的测试,描述所需功能
- 绿色:编写最小代码使测试通过
- 重构:改进代码结构,保持测试通过
示例 TDD 流程:
;; 红色阶段:编写失败的测试
(deftest test-calculate-discount
(is (= 90 (core/calculate-discount 100 10)))
(is (= 100 (core/calculate-discount 100 0))))
;; 绿色阶段:实现功能使测试通过
(defn calculate-discount
"计算折扣价格"
[original-price discount-percentage]
(let [discount (* original-price (/ discount-percentage 100))]
(- original-price discount)))
;; 重构阶段:改进实现
(defn calculate-discount
"计算折扣价格"
[original-price discount-percentage]
(* original-price (- 1 (/ discount-percentage 100.0))))
结语
clojure.test 库提供了编写 Clojure 程序测试所需的所有基本功能。通过掌握这些工具和技巧,你可以编写出可靠、维护性强且易于理解的测试,从而提高代码质量和开发效率。记住,测试不仅是验证代码正确性的手段,也是设计和文档的重要组成部分。

暂无评论,快来抢沙发吧!