Clojure 测试与质量工程:clojure.test、kaocha 与 CI 集成

深入 Clojure 测试与质量工程:clojure.test 与 kaocha 测试框架、expectations 行为风格、REPL 驱动测试、测试替身与依赖注入、数据库与外部服务测试、覆盖率与质量门禁、CI 流水线集成,构建从单元到系统的完整质量保障。

Clojure 以「REPL 驱动开发」著称——但这不代表它不需要系统化的测试工程。恰恰相反,Clojure 的不可变与纯函数特性让测试变得异常干净:没有 mock 泛滥、没有复杂的状态清理,测试代码几乎就是「输入 → 断言输出」。本文从测试框架、测试替身、外部依赖、覆盖率到 CI 门禁,搭建 Clojure 项目的完整质量体系,并与 /clojure-property-testing/ 的属性测试形成互补。

1. 测试框架选型

1.1 三个主流选择

框架风格特点
clojure.test官方、deftest/is最普及、工具集成好
kaocha测试运行器并行、CLI 友好、可扩展
expectations期望式断言语法更简洁、错误信息友好

推荐组合:clojure.test 写用例 + kaocha 作为运行器(支持并行、watch、CI 友好输出)。若团队喜欢更「声明式」的断言,再引入 expectations。

2. clojure.test 基础

2.1 单元测试

(ns order.core-test
  (:require [clojure.test :refer [deftest is testing]]
            [order.core :as core]))

(deftest discount-test
  (testing "VIP 用户折扣"
    (is (= 90 (core/apply-discount 100 {:tier :vip})))
    (is (= 100 (core/apply-discount 100 {:tier :normal}))))

  (testing "折扣不叠加到负数"
    (is (>= (core/apply-discount 10 {:tier :vip}) 0))))

(deftest edge-cases
  (is (nil? (core/apply-discount nil {})) "空订单返回 nil"))

2.2 测试运行

# clojure.test 内置运行
clojure -M:test

# kaocha 运行(支持 watch / fail-fast / 并行)
clojure -M:test -m kaocha.runner
clojure -M:test -m kaocha.runner --watch
clojure -M:test -m kaocha.runner --parallel

3. REPL 驱动测试工作流

REPL 驱动与测试并不矛盾——正确姿势是先写测试定义行为,再在 REPL 里让测试变绿:

1. 在 REPL 里写一个失败的测试(红)
2. 用 REPL 求值逐步实现(绿)
3. 重构时重复运行测试(重构)
;; 在 REPL 里交互式测试单个函数
(require '[order.core :as core])
(is (= 90 (core/apply-discount 100 {:tier :vip})))   ; REPL 直接断言
(run-tests 'order.core-test)                          ; 或跑整个命名空间

4. 测试替身与依赖注入

4.1 Clojure 为什么 mock 少

纯函数「输入 → 输出」,几乎不需要 mock。真正要处理的是副作用依赖(DB、HTTP、时钟),Clojure 惯用「依赖注入 + 动态绑定」:

;; 通过参数注入依赖(最干净)
(defn process-order [{:keys [db notifier clock]} order]
  (let [now ((:clock) ())
        saved (db/save! db (assoc order :paid_at now))]
    (notifier/notify! notifier saved)
    saved))

;; 测试时注入假实现
(process-order {:db      fake-db
                :notifier fake-notifier
                :clock    (constantly #inst "2026-09-28")}
               order)

4.2 with-redefs 处理全局依赖

;; 兜底手段:临时重定义全局函数
(defn fetch-rate [] (http/get "https://api.example.com/rate"))

(deftest rate-test
  (with-redefs [fetch-rate (fn [] {:status 200 :body 7.2})]
    (is (= 7.2 (core/convert-amount 100)))))

判断:优先「参数注入」,with-redefs 只用于遗留代码的测试缝——依赖注入让测试意图更清晰。

5. 外部依赖测试:Testcontainers

5.1 真库集成测试

数据库、消息队列等外部依赖,用 Testcontainers 起真实环境:

(require '[clj-test-containers.core :as tc])

(def pg
  (tc/create {:image-name "postgres:16"
              :exposed-port 5432}))

(tc/start! pg)
;; 拿到真实连接串,指向 Testcontainers 数据库
(def db-conn (-> pg tc/host (str ":" (tc/port pg))))

(deftest db-integration-test
  (let [conn (jdbc/get-datasource {:jdbcUrl (str "jdbc:postgresql://" db-conn "/test")})]
    (jdbc/execute! conn ["create table if not exists orders (...)"])
    (is (= 1 (count (core/query-orders conn))))))

(tc/stop! pg)

5.2 何时用真环境

依赖单元测试集成测试
纯函数✅ 完全 mock 掉—
数据库内存库/H2✅ Testcontainers 真库
消息队列—✅ Testcontainers Kafka
外部 HTTPmock 客户端契约测试 / 本地 stub

6. 覆盖率与质量门禁

6.1 覆盖率

;; cloverage 生成覆盖率报告
clojure -M:coverage

;; 结果:整体覆盖率 + 按命名空间
| namespace    | % forms | % lines |
|--------------|---------|---------|
| order.core   | 91.2    | 89.5    |
| order.db     | 78.3    | 75.0    |

要点:覆盖率是「测试是否覆盖代码」的度量,不是「质量」的度量。优先看关键路径与分支覆盖,而不是追求 100% 行覆盖。

6.2 质量门禁

# CI 中强制门禁:覆盖率不达标 → 构建失败
# kaocha + cloverage 组合
- run: clojure -M:coverage
  env:
    COVERAGE_THRESHOLD_LINE: "80"

7. CI 流水线集成

7.1 标准 CI 阶段

lint(clj-kondo)→ 单元测试(kaocha)→ 覆盖率(cloverage)→ 集成测试(Testcontainers)→ 构建 uberjar → 部署

7.2 GitHub Actions 示例

name: clojure-ci
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: '17' }
      - uses: DeLaGuardo/setup-clojure@12
        with: { tools-deps: '1.11.1' }
      - run: clojure -M:lint
      - run: clojure -M:test
      - run: clojure -M:coverage

8. 测试金字塔的 Clojure 落地

        /\            端到端(少量,Testcontainers + 真实服务)
       /  \
      /    \
     / 集成  \        ← 跨组件(DB + Kafka 真环境)
    /---------\
   /  单元测试  \     ← 大量(纯函数,秒级)
  /_____________\
层级数量速度依赖
单元多秒级无(纯函数)
集成中分钟级Testcontainers
端到端少慢完整环境

9. 常见陷阱

陷阱现象规避
测试依赖顺序单独跑失败每个测试自给自足
时间依赖断言跨午夜失败注入时钟(constantly)
覆盖率虚荣高覆盖仍漏关键关注关键路径分支
REPL 状态残留测试结果漂移每次测试重置全局 atom
不测错误路径只测 happy path显式测异常与边界

10. 总结

Clojure 测试工程的独特优势是纯函数让单元测试几乎零成本:依赖注入 + 参数化让 mock 最小化,REPL 让「红绿重构」循环顺畅,Testcontainers 让集成测试接近生产。落地组合拳:clojure.test 写用例 + kaocha 运行 + cloverage 覆盖门禁 + clj-kondo lint + CI 流水线;再用 /clojure-property-testing/ 的属性测试覆盖「无限边界」,用 /clojure-data-pipeline/ 的幂等思路保障数据侧。质量工程在 Clojure 里不是负担,而是函数式范式的自然产物。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「clojure」更多文章

  1. Clojure 函数式错误处理:Result、异常与结构化错误
  2. Clojure GraphQL API 实战:lacinia、Schema、Resolver 与权限
  3. Clojure REPL 驱动开发:nREPL、热重载与交互式工作流