文章目录

Clojure 测试:clojure.test 库

发布于 2026-04-09 20:13:33 · 浏览 4 次 · 评论 0 条

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"))))

测试 异常使用 isthrow? 结合:

(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.testtime 宏进行简单性能测试:

(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 中的应用

遵循 红色-绿色-重构的循环:

  1. 红色:编写一个失败的测试,描述所需功能
  2. 绿色:编写最小代码使测试通过
  3. 重构:改进代码结构,保持测试通过

示例 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 程序测试所需的所有基本功能。通过掌握这些工具和技巧,你可以编写出可靠、维护性强且易于理解的测试,从而提高代码质量和开发效率。记住,测试不仅是验证代码正确性的手段,也是设计和文档的重要组成部分。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文