Clojure单元测试规范
在Clojure中,clojure.test 是一个内置的测试框架,它提供了基本的工具来编写和执行测试。
作为Clojure标准库的一部分,clojure.test 提供了定义测试(deftest,testing)、设置前置和后置条件(fixture)、以及断言(is, are)这些基本机制。
而test runner是用来运行测试的工具。这些runner可以是独立的应用程序,或者是构建工具(比如Leiningen或Boot)的一部分。
Test runner的主要任务是自动发现、运行测试,并报告结果。它们通常提供更复杂的功能,如测试套件管理、测试结果格式化、并行测试执行等。
具体业务场景下, 我们可能需要自己实现测试执行器(特别是报告部分).
clojure.test 和各类test runner之间的关系可以这样理解:
测试定义:使用
clojure.test编写的测试包含了测试逻辑。这些测试定义了要测试的条件和预期的结果。测试执行:Test runner负责执行这些测试。它会加载测试代码,调用
clojure.test中的函数来运行这些测试,并收集结果。集成和兼容性:所有的test runner被设计为与
clojure.test兼容。这意味着,当你使用这些runner时,通常不需要对clojure.test编写的测试做任何特殊的修改。扩展和定制:虽然
clojure.test提供了基础功能,但某些复杂的测试场景可能需要更高级的功能,这通常是通过使用不同的test runner或者在clojure.test基础上进行扩展来实现的。
clojure.test 提供了测试的基础框架,而各种test runner则提供了执行这些测试的环境和工具。它们共同构成了Clojure中测试代码的生态系统。
在emacs中cider提供了和runner的集成. 本文使用cider中的runner来执行测试.
测试的基础知识
deftest,宏, 顶级测试区域, 可以含有多个testing.testing,宏, 定义一个测试区域中的小分组, 可以含有多个is,are.is,are, 宏, 定义了具体的断言- 简单的测试应该不需要预选准备测试环境, 也不需要清理测试现场, 所以不需要
use-fixtrues
(ns custombackend.modules.public.utils-format-test
(:require [custombackend.modules.public.utils-format :as sut]
[clojure.test :as t]))
(t/deftest test-trans-multi-tree
(t/testing "测试转化多级树-异常值"
(t/is (nil? (sut/trans-muilti-tree nil nil nil)))
(t/is (nil? (sut/trans-muilti-tree {} 1 {})))
(t/is (= {:a 2} (sut/trans-muilti-tree {:a 2} 1 {})))
(t/is (= {:total 0 :list []} (sut/trans-muilti-tree {:total 0 :list []} 2 {})))
(t/is (= {:total 1 :list {:a 1 :b "1" :c 0}}
(sut/trans-muilti-tree {:total 1 :list {:a 1 :b "1" :c 0}} 2 {:group :c})))
(t/is (= {:list [{:a 1, :b "1", :c 0}]}
(sut/trans-muilti-tree {:list [{:a 1 :b "1" :c 0}]} 1 {:group :c}))))
(t/testing "测试转化多级树-正常值"
(t/is (= {:total 1
:list [{:a 1 :b "1" :value 1 :lable "1" :c 0 :children [{:a 2 :b "2" :c 1}]}]}
(sut/trans-muilti-tree {:list [{:a 1 :b "1" :c 0}
{:a 2 :b "2" :c 1}]}
2
{:group :c :root 0 :value :a :lable :b})))
(t/is (= {:total 0 :list []} (sut/trans-muilti-tree {:total 0 :list []} 2 {:group :a})))
(t/is (= {:total 1
:list [{:a 1 :b "1" :value 1 :lable "1" :c 0 :children [{:a 2 :b "2" :c 1}]}]}
(sut/trans-muilti-tree {:total 2
:list [{:a 1 :b "1" :c 0}
{:a 2 :b "2" :c 1}]}
2
{:group :c :root 0 :value :a :lable :b})))))
测试的执行
如外部工具提供了Test Runner(Lein, Cider, cli tool)的情况下,我们可能不太关心测试的入口, 但如果想更灵活的控制测试执行, 或者自己实现一个Runner, 就需要深入了解下面的函数了.
test-var (test-var v)这个函数用于执行单个测试变量(通常是一个测试函数)。它接受一个变量 v,并运行该变量指向的测试。 适用场景:当你想单独运行一个特定的测试函数时。test-vars (test-vars vars)这个函数接受一个变量的集合,并对每个变量运行测试。这里的“变量”通常是指测试函数。 适用场景:当你有一组测试函数需要运行,但不需要运行整个命名空间中的所有测试时。test-ns (test-ns ns)这个函数用于运行给定命名空间 ns 中的所有测试。它会查找命名空间中所有标记为测试的变量,并运行它们。 适用场景:当你想运行一个特定命名空间(如一个文件或模块)中的所有测试时。test-all-vars (test-all-vars)这个函数在当前项目的所有命名空间中运行所有测试。它不需要任何参数。 适用场景:当你想运行项目中所有命名空间的所有测试时。run-tests (run-tests & namespaces)这个函数用于运行一个或多个命名空间中的测试。如果没有提供命名空间,它将运行所有命名空间中的测试。 适用场景:与 test-all-vars 类似,但提供了更多的灵活性,因为你可以指定要测试的命名空间。
测试的命名空间
clojure代码的组织是以命名空间(namespace)为单位的, 一般而言, 函数级的单元测试也是以对应原始代码的命名空间来组织.
以测试custombackend.modules.base.base-amdin-routes为例:
创建Test文件
假如base.base-amdin-routes文件路径是:custombackend/src/clj/custombackend/modules/base
需要在其对应的test路径:custombackend/test/clj/custombackend/modules/base下创建base-amdin-routes-test.clj的文件
使用 C-x C-f创建文件文件之后,默认声明了命名空间及测试依赖:
(ns custombackend.modules.base.base-amdin-routes-test
(:require [custombackend.modules.base.base-amdin-routes :as sut] ;; sut的意思是`symbol under test`
[clojure.test :as t]))
增加依赖
(ns custombackend.modules.base.base-amdin-routes-test
(:require [custombackend.handler :refer [app]]
[ring.mock.request :refer [request query-string json-body]]
[clojure.test :refer [deftest testing use-fixtures is]]
[mount.core :as mount]
[muuntaja.core :as m]
[user :refer [migrate-to reset-db]]
[custombackend.utils :refer [add-header]]))
因为是接口测试所以不需要应用被测文件的空间
添加use-fixtures
为了保证每次测试的独立性和可维护性,需要利用测试框架的use-fixtures准备测试环境, 比如外部服务, 恢复特定数据库状态.
(use-fixtures
:each
(fn [f]
(mount/start
#'custombackend.config/env
#'custombackend.db.core/*db*
#'custombackend.handler/app-routes
#'custombackend.modules.redis.redis-config/server1-conn)
(reset-db)
(f)))
- :once 对于命名空间中的所有deftest只执行一遍fixture中定于的事先和事后函数
- :each 对于命名空间中的每个deftest单独行一遍fixture中定于的事先和事后函数
编写测试用例
使用deftest测试一个业务的一组接口,包括增删改查;
使用testing定义一次接口请求的测试,并使用断言is进行判断
;;; 加急及试样接口测试
(deftest test-urgent-sample
(testing "测试分页获取接口1"
(migrate-to 20191212065536)
(let [response ((app) (-> (request :get "/admin/base/urgent-sample/list")
(query-string {"page" 0
"size" 2})
add-header))]
(is (= 200 (:status response)))
(is (= (test-data-query) (m/decode-response-body response))))))
- testing 的第一个参数是对当前这个测试Case的说明;剩下的表达式将以此执行
- 依赖当前DB数据的接口测试请添加migrate-to
migrate-to接受一个数字,表示回滚到migrate的哪个版本- 执行了
migrate-to之后将改变当前测试的DB数据,会影响之后的测试Case is接受两个参数,执行测试时,将根据第一个参数的结果值判断,如果是true表示测试通过;如果是false表示测试不通过,并将第二个参数的值输出在测试报告上- JSON数据格式化,使用
m/decode-response-body函数返回
然后编译C-c C-k
执行测试用例
- 方法一:在当前测试用例文件中,执行
C-c C-t n - 方法二:在被测文件中,执行
C-c C-t C-n - 方法三:命令行里,项目工程下,执行
lein test-refresh(建议长期保持)
执行结果查看
执行结果在emacs中可以直接看到
- 测试通过:

- 测试未通过:

私有方法的测试
跟公共方法类似,只不过需要使用with-private-fns包裹一下
例如custombackend.modules.public.utils-routes下的私有方法replace-slash
(utils/with-private-fns [custombackend.modules.public.utils-routes [replace-slash]]
(deftest test-replace-slash
(testing "测试 replace-slash"
(is (= "" (replace-slash "")))
(is (= "/api/path/query" (replace-slash "/api/path//query")))
(is (= "/api/path/query" (replace-slash "/api/path///query")))
(is (= "/api/path/query/" (replace-slash "////api////path///query//"))))))
项目中的使用规范
如果使用lein集成的runner.
lein test-refresh可以放到后台持续执行, 每次改动, 可以自动触发单元测试, 持续回归.- 每个接口完成开发后,必须编码相对应的UT.
- 当有数据库的读写操作时,要思考一下影响,根据情况考虑增加
migrate-to和reset-db