返回文章列表

文章

Clojure中怎么测试println输出内容

目录
  1. 1️⃣ 方法一:使用 with-out-str 捕获标准输出(推荐)
  2. 🔍 解析
  3. 2️⃣ 方法二:使用 with-redefs临时替换println(精确控制)
  4. 🔍 详细解析
  5. 1、创建 atom
  6. **2、临时替换 **println
  7. 3、添加 watcher 并修改 atom
  8. 4、断言
  9. 🧠 方法对比
  10. 3️⃣ 总结与推荐
  11. 📎 参考文章

在 Clojure 的单元测试中,我们通常使用 clojure.testis 来验证函数返回值。然而,当函数中存在 副作用(如 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 值,从 02,触发 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)))))

🔍 详细解析#

1、创建 atom#

(let [a (atom 0)
      called (atom nil)]
  • a:被监听的 atom
  • called:保存 println 的调用参数

**2、临时替换 **println#

(with-redefs [println (fn [& args] (reset! called args))]
  • with-redefs 会在其代码块执行期间临时重定义函数
  • println 替换为一个匿名函数,把参数存入 called,而不实际打印

3、添加 watcher 并修改 atom#

(add-watch a :print #(println "Changed from" %3 "to" %4))
(swap! a + 2)
  • swap! 修改值时,watcher 会调用我们临时重定义的 println
  • 参数 ["Changed from" 0 "to" 2] 被写入 called atom

4、断言#

(is (= ["Changed from" 0 "to" 2] @called))
(is (= 2 @a))
  • 验证捕获到的参数是否正确
  • 验证 atom 的最终值

🧠 方法对比#

方法原理优点缺点适用场景
with-out-str捕获标准输出流简单直观输出带换行符,难区分多次调用验证输出文本
with-redefs临时替换函数精确断言参数代码稍繁琐验证参数或禁止真实输出

3️⃣ 总结与推荐#

  • 一般场景 → 用 with-out-str,快速捕获输出文本
  • 需要精确断言参数或禁止打印 → 用 with-redefs
  • add-watch 回调参数 %3%4 分别是变化前后的值,便于在 watcher 中断言

💡 小技巧:封装宏提高可读性

(defmacro is-printed [expected & body]
  `(let [out# (with-out-str ~@body)]
     (is (clojure.string/includes? out# ~expected))))

使用:

(deftest demo
  (is-printed "Hello" (println "Hello, world!")))

这样,你就可以在 Clojure 测试中安全、准确地验证 println 输出内容,无论是捕获文本还是捕获参数。

📎 参考文章#