在 Clojure 的单元测试中,我们通常使用 clojure.test 的 is 来验证函数返回值。然而,当函数中存在 副作用(如 println 输出)时,如何在测试中断言它的输出呢?

本文将介绍两种方法:

  1. 捕获标准输出(with-out-str)
  2. 临时重定义 println 捕获参数(with-redefs)

1️⃣ 方法一:使用 with-out-str 捕获标准输出(推荐)

with-out-str 会捕获代码块中所有打印到标准输出的内容,并返回字符串。

示例:

(deftest atom-watcher
  (testing "use watcher"
    (let [a (atom 0)
          out (with-out-str                      ;; 捕获 println 输出
                (add-watch a :print
                           #(println "Changed from" %3 "to" %4))
                (swap! a + 2))]                   ;; 触发 watcher
      ;; 验证 atom 的值
      (is (= 2 @a))
      ;; 验证输出内容是否包含预期字符串
      (is (re-find #"Changed from 0 to 2" out)))))

🔍 解析

  1. (with-out-str ...)

    捕获所有标准输出,并将其作为字符串返回到 out。

  2. (add-watch a :print #(println "Changed from" %3 "to" %4))

    添加 watcher,当 atom 值变化时触发,打印变化前后的值。

  3. (swap! a + 2)

    修改 atom 值,从 0 → 2,触发 watcher。

  4. (is (re-find ... out))

    使用正则匹配输出字符串,验证输出内容是否符合预期。

注意:输出可能包含换行符,用 re-find 或 clojure.string/includes? 更稳妥。


2️⃣ 方法二:使用 with-redefs临时替换println(精确控制)

如果你想完全不产生真实输出,而是直接捕获传给println的参数,可以使用 with-redefs。

示例:

(deftest atom-watcher
  (testing "use watcher with redefs"
    (let [a (atom 0)
          called (atom nil)]                     ;; 用于保存 println 调用参数
      (with-redefs [println (fn [& args] (reset! called args))] ;; 临时替换 println
        (add-watch a :print #(println "Changed from" %3 "to" %4))
        (swap! a + 2))                          ;; 触发 watcher
      ;; 断言捕获到的参数是否正确
      (is (= ["Changed from" 0 "to" 2] @called))
      ;; 断言 atom 的值
      (is (= 2 @a)))))