文章
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 ...)))) |