返回文章列表

文章

Clojure中的单元测试

1. 测试纯函数 如果你的函数是纯函数(只依赖于输入参数,没有副作用),那么测试它非常简单。你只需要调用该函数并使用 clojure.test 中的 is 宏来断言结果。 示例代码:

(ns my-app.core
  (:require [clojure.string :as str]))

(defn process-name [full-name]
  (let [[first-name last-name] (str/split full-name #" ")]
    (str "Hello, " first-name "!")))

测试代码:

(ns my-app.core-test
  (:require [clojure.test :refer [deftest is]]
            [my-app.core :as sut])) <i>; sut 是 "System Under Test" 的缩写</i>

(deftest test-process-name
  (is (= "Hello, John!" (sut/process-name "John Doe")))
  (is (= "Hello, Jane!" (sut/process-name "Jane Smith"))))

2. 测试有副作用的函数 如果 let 绑定的值是来自有副作用的操作(如文件读写、数据库调用或网络请求),你应该遵循以下方法: 隔离依赖,传递依赖 将有副作用的部分作为参数传递给函数。这使得函数的核心逻辑保持"纯粹",易于测试。 示例代码:

(defn process-with-db [get-user-fn user-id]
  (let [user (get-user-fn user-id)]
    (str "User details: " (:name user))))

测试代码:

(ns my-app.core-test
  (:require [clojure.test :refer [deftest is]]
            [my-app.core :as sut]))

(deftest test-process-with-db
  (let [mock-get-user (fn [id] {:id id :name "Test User"})]
    (is (= "User details: Test User" (sut/process-with-db mock-get-user 123)))))

在这个例子中,我们传递一个模拟函数 mock-get-user 来代替真正的数据库访问,这使得测试变得可预测且独立。 使用 with-redefs 如果无法修改函数签名来传递依赖,你可以使用 clojure.core/with-redefs 宏。它允许你在测试执行期间临时重新定义一个 Var,例如一个公共函数。但请注意,这种方法被一些人认为是一种"黑科技"。 示例代码:

(ns my-app.data)

(defn fetch-data [id]
  ;; 假设这是一个访问外部服务的函数
  (throw (Exception. "Should not be called in tests")))
(ns my-app.core
  (:require [my-app.data :as data]))

(defn process-data [id]
  (let [data-item (data/fetch-data id)]
    (str "Processed: " data-item)))

测试代码:

(ns my-app.core-test
  (:require [clojure.test :refer [deftest is testing]]
            [my-app.core :as sut]
            [my-app.data :as data]))

(deftest test-process-data
  (testing "使用 with-redefs 模拟外部依赖"
    (with-redefs [data/fetch-data (fn [id] (str "mocked-data-" id))]
      (is (= "Processed: mocked-data-456" (sut/process-data 456))))))

3. 测试私有函数(defn- 如果 let 表达式位于一个私有函数(使用 defn- 定义)内部,最佳做法仍然是通过测试调用该私有函数的公共函数来验证结果。私有函数是实现细节,不应直接暴露给外部测试。 如果你确实需要测试一个私有函数,可以利用 Clojure 的灵活性,使用 #' 读取器宏来获取私有函数的 Var示例代码:

(ns my-app.internal)

(defn- calculate-total [items]
  (let [base-price (reduce + (map :price items))
        tax (* base-price 0.1)]
    (+ base-price tax)))

(defn get-order-total [items]
  (calculate-total items))

测试代码:

(ns my-app.internal-test
  (:require [clojure.test :refer [deftest is]]
            [my-app.internal :as sut]))

(deftest test-calculate-total
  (let [private-calculate-total (var-get #'my-app.internal/calculate-total)]
    (is (= 110.0 (private-calculate-total [{:price 100}]))))
  (let [items [{:price 50} {:price 50}]]
    (is (= 110.0 (sut/get-order-total items)))))

总结

场景推荐做法示例
纯函数直接测试包含**let**的公共函数。(is (= "..." (my-fn ...)))
有副作用传递依赖作为函数参数,测试核心纯逻辑。(is (= "..." (my-fn mock-dependency ...)))
有副作用(无法传递)使用**with-redefs**宏模拟外部依赖。(with-redefs [real-fn mock-fn] ...)
私有函数通过测试公共 API 来验证其行为,私有函数不应独立测试。(is (= "..." (my-public-fn ...)))
私有函数(特殊情况)使用**#'获取私有函数的Var****,用于临时测试。**(let [pfn (var-get #'ns/pfn)] (is (= "..." (pfn ...))))

📎 参考文章#