October 11, 2023
By: Kevin

Electron前端单元测试框架

  1. 框架概述
  2. 跨操作系统的系统级函数
    1. win? 函数
  3. 进程管理与命令执行
    1. run-cmd 函数
    2. sh-perline 函数
    3. 进程管理函数
  4. 日志处理与StepReader协议
    1. StepReader协议
    2. LogReader实现
    3. 日志处理函数
  5. 版本检查与依赖管理
    1. compare-versions 函数
    2. check-version 函数
  6. 测试报告生成与集成
    1. 报告生成逻辑
    2. 报告生成函数
      1. gen-file 函数
      2. org-insert-txt 函数
      3. org-insert-image 函数
      4. 报告文件结构与分段
    3. 测试宏扩展
      1. deftest-with-rpt
      2. testing-with-rpt
    4. 报告内容插入示例
  7. 通知与自动化
    1. 发送钉钉消息
    2. 生成Markdown消息
    3. 发送报告消息
  8. 结论
  9. 参考资料

report

框架概述

该单元测试框架旨在提供一套全面的工具集, 专注于前端交互测试, 同时集成了报告生成, 系统管理, 日志收集和通知机制等关键功能. 框架主要由以下两个核心部分组成:

  • 前端交互工具库: 该库专门解决前端交互的问题, 包括元素点击, 页面滚动等操作. 通过简洁的API, 开发者可以轻松模拟用户行为, 进行界面测试. 这部分功能确保了测试脚本能够高效, 准确地与前端应用进行交互.
  • 综合测试管理框架: 在前端交互工具库的基础上, 框架进一步提供了报告生成, 系统管理, 日志收集和通知机制等功能. 这些功能并不包含在前端交互工具库中, 而是作为框架的附加特性, 为开发者提供一个全面的测试解决方案. 这部分功能的集成使得整个测试流程更加自动化和系统化, 大大提升了测试的效率和可靠性.

特性

  • 环境版本检查与依赖管理: 在测试前, 框架会自动检查环境中各个工具和依赖的版本, 确保测试环境的稳定性.
  • 跨操作系统兼容性: 无论是在Windows还是Unix系统上, 框架都能稳定运行, 确保测试流程的一致性.
  • 自动化报告生成: 通过集成org-mode, 框架能够自动生成结构化(带有目录, 日志和截图)的测试报告(html或者pdf), 便于分析和分享测试结果.
  • 实时日志处理: 框架能够实时监控和处理日志信息, 帮助开发者快速定位问题.
  • 通知机制: 集成了钉钉等通知平台, 测试完成后可自动发送报告, 确保团队成员及时获取最新的测试结果.

report

跨操作系统的系统级函数

为了确保框架在不同操作系统(windows/macos)下的兼容性, 首先需要判断当前运行的操作系统类型, 并根据不同的系统执行相应的命令. 框架中提供了以下几个基础函数:

win? 函数

用于判断当前操作系统是否为Windows 10或Windows 11.

(defn win?
  "检查当前操作系统是否为Windows
  - 无参数
  - 返回布尔值, 如果是Windows 10/11则返回true, 否则返回false"
  []
  (let [os-name (System/getProperty "os.name")]
    (or (= "Windows 10" os-name)
        (= "Windows 11" os-name))))

进程管理与命令执行

跨平台的命令执行和进程管理是测试框架的重要组成部分. 框架提供了多种方式来执行命令, 管理进程以及获取进程信息.

run-cmd 函数

以阻塞的方式同步的方式执行命令, 命令进程退出(exit)之后, 一次性输出结果, 才能继续执行.

执行指定的命令并返回执行结果. 根据操作系统的不同, 选择合适的命令执行方式(windows用powershell, 其他系统都自带shell).

(defn run-cmd
  "执行指定的命令并返回执行结果
  - 参数 `cmd`: 字符串, 要执行的命令
  - 返回一个包含命令执行结果的映射, 包括: `:out`(标准输出), `:err`(错误输出)和`:exit`(退出状态)"
  [cmd]
  (if (win?)
    (sh "powershell" "Invoke-Expression" (format "'%s'" cmd))
    (sh "sh" "-c" cmd)))

sh-perline 函数

执行过程中, 实时逐行输出命令的标准输出, 适用于需要实时监控命令执行过程的场景.

(defn sh-perline
  "执行指定命令并实时逐行输出其标准输出
  - 参数 `cmd-str`: 字符串, 要执行的命令
  - 返回进程对象"
  [cmd-str]
  (let [cmds (if (win?)
               ["powershell" "Invoke-Expression" (format "'%s'"cmd-str)]
               ["sh" "-c"  (str "exec " cmd-str)])
        process-builder (ProcessBuilder. (into-array String cmds))
        process (.start process-builder)]
    (println "进程号是" (.pid process))
    (future (with-open [reader (BufferedReader. (InputStreamReader. (.getInputStream process)))]
              (doseq [line (line-seq reader)]
                (println line))))
    process))

进程管理函数

测试前端过程中, 需要对后端进行检测和管理, 有时候需要启停某些进程. 以下是一组工具函数.

  • get-pids: 根据正则表达式查找匹配的进程ID列表.
  • get-lein-pids: 获取运行Leiningen的所有进程ID.
  • pkill: 根据提供的进程ID列表杀死相应的进程.
  • start-allkill-all: 分别用于启动和杀死所有Leiningen进程.
(defn get-pids
  "根据正则表达式查找匹配的进程ID列表
  - 参数 `regx`: 字符串, 用于匹配进程信息的正则表达式
  - 返回匹配的进程ID列表"
  [regx]
  (let [jps (run-cmd (format "jps -v | grep %s" regx))]
    (->> jps
         :out
         (str/split-lines)
         (mapv #(str/split % #" "))
         (mapv first)
         (filter (complement empty?)))))
(defn get-lein-pids
  "获取运行Leiningen的所有进程ID
  - 无参数
  - 返回运行Leiningen的进程ID列表"
  []
  (get-pids "dev-config.edn"))
(defn pkill
  "根据提供的进程ID列表杀死相应的进程
  - 参数 `pids`: 进程ID列表
  - 无返回值"
  [pids]
  (run! #(run-cmd (if (win?)
                    (format "taskkill /F /PID %s" %1)
                    (format "kill -9 %s" %1)))
        pids))
(defn start-all
  "使用Leiningen启动所有项目, 并记录日志
  - 参数 `log`: 日志文件名
  - 无返回值"
  [log]
  (sh-perline (format "lein run %s" log)))
(defn kill-all
  "杀死所有Leiningen进程
  - 无参数
  - 无返回值"
  []
  (pkill (get-lein-pids)))

日志处理与StepReader协议

日志是测试过程中重要的信息来源. 框架通过定义StepReader协议和相关函数, 实现了对日志的实时监控和处理.

能够做到testcase和日志的精确对应, 极大提升了测试后failed case的分析效率.

StepReader协议

定义了用于日志处理的基本函数, 包括启动日志读取, 获取当前执行的函数, 获取下一步骤以及获取下一个步骤的超时处理.

此外有些case的成功/失败判断经由日志中的结果, 更容易判断.

(defprotocol StepReader
  "定义了StepReader协议, 包含用于日志处理的函数
  - `start`: 启动日志读取
  - `current-function`: 获取当前执行的函数
  - `next-step`: 获取下一个步骤
  - `next-step-timeout`: 获取下一个步骤 "
  (start [this] "启动日志读取流程")
  (current-function [this] "返回当前正在执行的函数")
  (next-step [this] "返回下一个执行步骤")
  (next-step-timeout [this] "超时时间内返回下一个执行步骤"))

LogReader实现

通过LogReader记录器, 实现了StepReader协议, 对日志文件进行实时监控, 并通过core.async通道传递日志信息.

(defrecord LogReader [log-file interval func-chan step-chan]
  StepReader
  (start [_] (let [f (fn [file-path interval]
                       (let [file (File. file-path)
                             reader (BufferedReader. (FileReader. file))
                             init-len (.length file)]
                         (.skip reader init-len)
                         (loop [last-len init-len]
                           (Thread/sleep interval)
                           (let [current-len (.length file)]
                             (when (< last-len current-len)
                               (loop []
                                 (let [line (.readLine reader)]
                                   (when line
                                     (let [[f_ f-name] (fun-name line)]
                                       (when (seq f-name)
                                         (timbre/info "Put Value Into Func-c")
                                         (async/put! func-chan f-name)))
                                     (when-let [_step (step? line)]
                                       (async/put! step-chan line))
                                     (recur)))))
                             (recur current-len)))))]
                   (future (f log-file 1000))))
  (current-function [_] (fn [] (async/<!! func-chan)))
  (next-step [_]        (fn [] (async/<!! step-chan)))
  (next-step-timeout [_] (fn [timeout]
                           (let [[v _c] (async/alts!! [(async/timeout
                                                        (* 1000  timeout))
                                                       step-chan])]
                             (or v (throw (ex-info "等待日志超时" {})))))))

日志处理函数

  • fun-name: 从日志行中提取函数名.
  • step?: 检查日志行是否表示一个步骤.
  • log-handler: 处理日志文件, 返回获取当前函数和下一步骤的函数.
(defn fun-name
  "从日志行中提取函数名
  - 参数 `log-line`: 日志中的一行
  - 返回该行中提到的函数名"
  [log-line]
  (first (re-seq #"([A-zA-z>0-9-]+) Starts" log-line)))
(defn step?
  "检查日志行是否表示一个步骤
  - 参数 `log-line`: 日志中的一行
  - 返回布尔值, 指示该行是否包含步骤信息"
  [log-line]
  (let [res [#"open valves:"
             #"open pumps:"
             #"close valves"
             #"close pumps:"
             #"([A-zA-z>0-9-]+) Ends"
             #"interval"
             #"speed"
             #"motor"
             #"receive-M1"
             #"receive-M2"] ;; 此处保留各类扩展
        fs  (mapv #(partial re-seq %) res)]
    (some (complement nil?) (mapv #(% log-line) fs))))
(defn log-handler
  "处理日志文件, 返回两个函数来获取当前执行的函数和下一步骤
  - 参数 `log-file`: 日志文件路径
  - 参数 `interval`: 检查新日志的时间间隔(毫秒)
  - 返回三个个函数: 1. 获取当前函数, 2. 获取下一步骤 3. 必须在x秒之内获得下一步骤"
  [log-file interval]
  (let [r (->LogReader log-file interval (async/chan (async/sliding-buffer 1)) (async/chan))]
    (start r)
    [(current-function r) (next-step r) (next-step-timeout r)]))

版本检查与依赖管理

确保测试环境中的各个工具和依赖版本符合要求, 是保证测试稳定性的重要步骤. 框架提供了一系列函数, 用于检查Java, Node.js, Leiningen等工具的版本.

compare-versions 函数

用于比较两个版本号, 支持大版本.小版本.编译序号的格式.

(defn compare-versions
  "比较两个版本号, 版本号构成`大版本.小版本.编译序号`
  - 参数 `v1`和`v2`: 字符串, 表示要比较的版本号
  - 返回值: 如果v1较新, 返回1; 如果相同, 返回0; 如果v2较新, 返回-1"
  [v1 v2]
  (let [parts1 (map read-string (clojure.string/split v1 #"\."))
        parts2 (map read-string (clojure.string/split v2 #"\."))
        comparison (map compare parts1 parts2)]
    (cond
      (some pos? comparison) 1  ; v1 is greater
      (some neg? comparison) -1 ; v2 is greater
      :else 0)))               ; versions are equal

check-version 函数

检查所有依赖的版本是否符合要求, 并输出检查结果.

(defn check-version
  "检查所有依赖的版本是否符合要求
  - 无参数
  - 无返回值, 但会打印每个依赖的版本检查结果"
  []
  (run-version-check "node --version" #"v(.*)" "16.0.0" "node.js")
  (run-version-check "lein --version" #"Leiningen ([0-9.]+)" "2.10.0" "lein")
  (run-version-check "bb --version" #"babashka v(.*)" "1.3.100" "bb")
  (run-version-check "clj-kondo --version" #"clj-kondo v(.*)" "2023.10.20" "clj-kondo")
  (java-verison))

测试报告生成与集成

为了更好地展示测试结果, 框架集成了org-mode, 并提供了生成和插入测试报告的功能. 最终的测试报告以org-mode格式生成, 便于在Emacs或其他支持org-mode的编辑器中查看和管理.

报告生成逻辑

测试报告的生成逻辑主要包括以下几个步骤:

  1. 初始化报告文件: 使用gen-file函数, 根据测试名称生成一个新的org-mode报告文件, 并插入主题和测试标题.
  2. 插入测试内容: 在测试过程中, 通过org-insert-txtorg-insert-image函数将测试结果和截图插入到报告文件中.
  3. 结构化分段: 报告文件按照测试的层级结构进行分段, 使用*, **org-mode的标题级别来区分不同的测试部分和细节.

报告生成函数

gen-file 函数

用于生成新的org-mode报告文件, 并初始化文件内容.

(defn gen-file "使用`test-name`生成报告文件, 主题为文件的一级目录为`test-name`"
  [test-name]
  (let [file-name (org-file-name test-name)]
    (fs/delete-if-exists file-name)
    (spit file-name (str
                     org-header
                     "* " test-name "\n"))))

org-insert-txt 函数

在报告文件中插入文本内容, 支持插入测试描述, 结果等信息.

(defn org-insert-txt "在`test-name`测试报告中插入文本`txt`"
  [test-name txt]
  (let [file-name (org-file-name test-name)]
    (spit file-name (str txt "\n") :append true)))

org-insert-image 函数

在报告文件中插入图片截图, 便于直观展示测试过程中出现的问题或关键步骤.

(defn org-insert-image "在测试报告中插入图片"
  [test-name img-name]
  (let [image-path (str/replace (image-path test-name img-name) "target/" "")
        file-name (org-file-name test-name)]
    (spit file-name (str
                     "#+attr_html: :width 800px\n #+attr_latex: :width 800px\n #+ATTR_LATEX: :width 15cm\n"
                     "[[file:" image-path "]]\n")  :append true)))

报告文件结构与分段

org-mode报告文件的结构化分段如下:

  • 一级标题(*): 测试名称, 作为报告的主要标题.
  • 二级标题(**): 测试的各个部分或模块, 例如不同的功能测试, 集成测试等.
  • 三级标题及以下(***, **** 等): 具体的测试用例或步骤, 详细描述每个测试的执行情况, 结果和截图.

例如:

#+SETUPFILE: https://gitee.com/zhaoyuKevin/org-theme/raw/master/org/theme-readtheorg.setup

* 多肽测试报告
** 用户登录测试
*** 测试用例1: 成功登录
- 测试步骤:
  1. 打开登录页面
  2. 输入用户名和密码
  3. 点击登录按钮
- 预期结果: 登录成功, 跳转到首页
- 实际结果: 登录成功
[[file:target/multipeptide-test/2024-04-27-123456-789012.png]]

*** 测试用例2: 登录失败
- 测试步骤:
  1. 打开登录页面
  2. 输入错误的用户名和密码
  3. 点击登录按钮
- 预期结果: 显示错误提示信息
- 实际结果: 显示错误提示信息
[[file:target/multipeptide-test/2024-04-27-123457-789013.png]]

测试宏扩展

为了简化测试过程中报告的生成和更新, 框架提供了两个宏: deftest-with-rpttesting-with-rpt, 它们扩展了clojure.test的功能, 在测试过程中自动生成和更新测试报告.

deftest-with-rpt

扩展了clojure.test/deftest, 在测试开始时插入测试标题, 并在测试结束后记录日志.

(defmacro deftest-with-rpt [name & body]
  `(clojure.test/deftest ~name
     (let [bc (backend-log-count)
           pc (plc-log-count)]
       (utils/org-insert-txt *test-name* (str "** " '~name))
       ~@body
       #_(when-not (zero? pc)
           (let [logs (drop pc (all-plc-log))]
             (utils/org-insert-txt *test-name* (plc-logs 3 logs))))
       #_(when-not (zero? bc)
           (let [logs (drop bc (all-backend-log))]
             (utils/org-insert-txt *test-name* (backend-logs 3 logs))))))

testing-with-rpt

扩展了clojure.test/testing, 在每个测试步骤中插入相应的报告内容, 包括步骤描述和日志信息.

(defmacro testing-with-rpt [name & body]
  `(clojure.test/testing ~name
     (let [bc (backend-log-count)
           pc (plc-log-count)]
       (utils/org-insert-txt *test-name* (str "*** " ~name))
       (try
         ~@body
         (catch Exception e
           (println "exception !!!!!:" (ex-data e))))
       (when-not (zero? pc)
         (let [logs (remove-query (drop pc (all-plc-log)))]
           (utils/org-insert-txt *test-name* (plc-logs 4 logs))))
       (when-not (zero? bc)
         (let [logs (drop bc (all-backend-log))]
           (utils/org-insert-txt *test-name* (backend-logs 4 logs))))))

报告内容插入示例

在测试用例中使用这些宏, 可以自动将测试结果和日志信息插入到报告文件中.

(deftest-with-rpt 用户登录测试
  (testing-with-rpt "成功登录"
    ;; 测试逻辑
    (is-with-rpt (login? driver) "登录应成功"))
  (testing-with-rpt "登录失败"
    ;; 测试逻辑
    (is-with-rpt (not (login? driver)) "登录应失败")))

上述代码将在报告文件中生成如下结构:

* 多肽测试报告
** 用户登录测试
*** 成功登录
| 测试表达式 | 结果 | 备注 |
|" (login? driver)" |✅  | 登录应成功 |
*** 登录失败
| 测试表达式 | 结果 | 备注 |
|" (not (login? driver))" |✅  | 登录应失败 |

并根据测试过程中生成的截图和日志信息, 自动插入相应的内容.

通知与自动化

测试完成后, 框架支持通过钉钉(DingTalk)发送测试报告和统计信息, 方便团队及时了解测试结果.

发送钉钉消息

通过定义钉钉机器人的Webhook地址, 框架可以将生成的测试报告发送到指定的钉钉群.

(defn ding-md
  "发送叮叮消息"
  [hook msg]
  (http/post hook
             {:headers {:content-type "application/json"}
              :body (json/encode msg)}))

生成Markdown消息

将测试报告转换为Markdown格式, 以便在钉钉中以美观的方式展示.

(defn package->md-msg []
  {:msgtype "markdown"
   :at {:isAtAll true}
   :markdown {:title "多肽版本发布"
              :text (format "## 多肽版本发布
- 请到 http://192.168.1.129/ 登录下载

## git信息
%s
" (log-md-format (git-log)))}})

发送报告消息

通过调用相应的函数, 将生成的Markdown消息发送到钉钉群.

(defn send-package-msg []
  (when-let [msg (package->md-msg)]
    (ding-md ut-hook msg)))

(defn send-xray-msg []
  (when-let [msg (xray-org-file->md-msg xray-org)]
    (ding-md xray-hook msg)))

结论

通过上述功能模块的整合, 这个基于Clojure的单元测试封装框架不仅提升了测试的自动化和跨平台兼容性, 还通过实时日志处理和详细的测试报告生成, 帮助开发团队更高效地发现和解决问题. 同时, 集成的通知功能确保测试结果能够及时传达到相关人员, 进一步优化了开发和测试流程.

参考资料

Tags: clojure ut