December 20, 2019
By: Kevin

Clojure单元测试规范

  1. 测试的基础知识
  2. 测试的命名空间
  3. 项目中的使用规范

在Clojure中,clojure.test 是一个内置的测试框架,它提供了基本的工具来编写和执行测试。

作为Clojure标准库的一部分,clojure.test 提供了定义测试(deftest,testing)、设置前置和后置条件(fixture)、以及断言(is, are)这些基本机制。

而test runner是用来运行测试的工具。这些runner可以是独立的应用程序,或者是构建工具(比如Leiningen或Boot)的一部分。

Test runner的主要任务是自动发现、运行测试,并报告结果。它们通常提供更复杂的功能,如测试套件管理、测试结果格式化、并行测试执行等。

具体业务场景下, 我们可能需要自己实现测试执行器(特别是报告部分).

clojure.test 和各类test runner之间的关系可以这样理解:

  1. 测试定义:使用clojure.test编写的测试包含了测试逻辑。这些测试定义了要测试的条件和预期的结果。

  2. 测试执行:Test runner负责执行这些测试。它会加载测试代码,调用clojure.test中的函数来运行这些测试,并收集结果。

  3. 集成和兼容性:所有的test runner被设计为与clojure.test兼容。这意味着,当你使用这些runner时,通常不需要对clojure.test编写的测试做任何特殊的修改。

  4. 扩展和定制:虽然clojure.test提供了基础功能,但某些复杂的测试场景可能需要更高级的功能,这通常是通过使用不同的test runner或者在clojure.test基础上进行扩展来实现的。

clojure.test 提供了测试的基础框架,而各种test runner则提供了执行这些测试的环境和工具。它们共同构成了Clojure中测试代码的生态系统。

emacscider提供了和runner的集成. 本文使用cider中的runner来执行测试.

测试的基础知识

  1. deftest,宏, 顶级测试区域, 可以含有多个testing.
  2. testing,宏, 定义一个测试区域中的小分组, 可以含有多个is, are.
  3. is,are, 宏, 定义了具体的断言
  4. 简单的测试应该不需要预选准备测试环境, 也不需要清理测试现场, 所以不需要 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.

  1. lein test-refresh 可以放到后台持续执行, 每次改动, 可以自动触发单元测试, 持续回归.
  2. 每个接口完成开发后,必须编码相对应的UT.
  3. 当有数据库的读写操作时,要思考一下影响,根据情况考虑增加 migrate-toreset-db
Tags: clojure ut