单元测试:从 Jest 到 Karma+Mocha+Chai

我所参与过的 Web 项目大部分都使用 React 框架。Jest 是我们曾经最主要使用的单元测试 (Unit Test) 框架。Jest 配置足够简单,功能也足够丰富。但是随着前端开发逐渐进入深水区,Jest 的短板开始逐渐显现。我在经过探索和尝试后,迁移到了 Karma+Mocha+Chai 的解决方案。在此分享这两种技术方案的区别和取舍,希望对遇到同样问题的同学有所帮助。

Jest 是 Facebook 开发的一体式单元测试工具。你不需要关心什么是测试运行环境,什么是测试框架,什么是断言库。Jest 就是你所需要的全部。它只需要非常少的配置,几乎做到了开箱即用。Jest 随着 React 生态的繁荣而大行其道,然而它并不是那么完美。Jest 的运行环境是 JSDOM,一个伪 DOM 引擎。

JSDOM 不能理解 DOM 的布局,尺寸,样式,以及浏览器的高级 API,例如 ResizeObservermatchMedia()。我们无法在代码中避免使用它们,于是只能去 Mock 很多东西。结果,我们欺骗了我们自己写的测试。即使你达成了 100% 测试覆盖,所有测试用例都通过,还是会怀疑代码是否在真实环境中会出错…更不要提浏览器兼容性测试了。

Karma 是 Google 开发的测试运行工具。它在 Angular 社区很知名,且仍然是现存最好的在真实浏览器中运行单元测试的工具。当人们无法忍受太多 Mock,他们通常会转投 Karma 然后发现它真香!你不需要再 Mock 浏览器的特性,便可获得更真实的测试结果,甚至能做浏览器兼容性测试。更重要的是,从 Jest 到 Karma 的迁移并不难。

Karma 不能单独使用。你至少需要一个测试框架(例如 Mocha)和一个断言库(例如 Chai)。现有的 Jest 单元测试只需批量替换一些 Jest 断言函数到 Chai 断言函数即可。

本文对应的代码库为 https://github.com/guoyunhe/jest-vs-karma 分支 main 为 jest 测试,分支 karma 为 karma 测试。两种方案均为纯 TypeScript 项目。

工具集对比

Karma 系Jest 系
测试运行环境 Test RuntimeChrome/Firefox/Safari/Edge/IEJSDOM + Node.js
代码转译器 TranspilerTypeScriptTypeScript
测试运行工具 Test RunnerKarmaJest
测试框架 Test FrameworkMocha/JasmineJest
断言库 Assertion LibraryChai/Expect.jsJest
React 测试工具 React Test UtilityEnzyme/React-Testing-LibraryEnzyme/React-Testing-Library
两种单元测试工具集配置的比较

测试运行环境(Test Runtime)是单元测试代码最终执行的环境。前端项目的运行环境,主要分为 Node.js 模拟环境和浏览器真实环境。Node.js 模拟环境轻量适合流水线,但是和真实浏览器行为有一定差距,且随着 Web 标准发展逐渐拉大。真实浏览器环境虽然占用更多的存储和计算资源,但更接近于实际情况,且能够做浏览器兼容性测试。由于 JSDOM 是用 JS 实现的 DOM 引擎,相比原生 C++ 实现的排版引擎性能低得多,在实际运行速度上不及浏览器,组件越复杂差距越明显。因此我个人认为,真实浏览器环境是不二之选。

测试运行工具(Test Runner)是负责解析和调用单元测试代码并加载测试运行环境(Test Runtime)的工具。Jest 和 Karma 的架构设计都是支持多种运行环境的。因为 Facebook 内部并不使用浏览器运行时做单元测试(详情),Jest 官方只提供了 JSDOM 支持,且短期内不会改变。而 Karma 则生而为浏览器运行环境设计,官方提供了成熟的 Chrome,Firefox,Safari,Edge 和 IE 支持。Karma 之先进,Jest 不能望其项背。

测试框架(Test Framework)简单说就是提供了 describe()it() 函数的工具。Jest 本身也是一个测试框架。而 Karma 则把这部分职责交给了 Mocha 或 Jasmine 之类成熟的测试框架。对开发者来说,它们的差异是很难感知的,语法相同,且都是优秀的测试框架。

而断言库(Assertion Library)直白来讲就是 expect() 函数的提供者。Jest 本身就提供了断言库的能力。而 Karma 则再次将这部分分配给第三方的断言库,比如 Chai。Jest 和 Chai 的断言语法略有不同,但是没有优劣之分,迁移也不困难。

在此之上的测试工具,比如 React 单元测试常用的 Enzyme 和 React-Testing-Library,都是通用的。

Karma 甚至有对 Jest 的第三方支持库,以实现和 Jest 完全兼容,可以无缝迁移的解决方案。但是尚不成熟,不建议在实际项目中使用。

项目迁移流程

依赖安装

系统需要安装下列任意一种浏览器:Chrome/Chromium,Edge,Firefox,IE,Safari。如果需要兼容性测试,可以配置多个 CI 容器(Linux/macOS/Windows),运行不同的浏览器测试。

安装项目依赖:

npm r -D jest ts-jest @types/jest
npm i -D karma karma-chrome-launcher karma-mocha karma-typescript chai mocha @types/chai @types/karma @types/mocha

配置文件迁移

删除 Jest 的测试配置 jest.config.js 。创建 Karma 测试配置 karma.conf.js

module.exports = function (config) {
  config.set({
    frameworks: ["mocha", "karma-typescript"],
    files: ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts", "test/**/*.tsx"],
    preprocessors: {
      "**/*.ts": "karma-typescript",
      "**/*.tsx": "karma-typescript",
    },
    reporters: ["progress", "karma-typescript"],
    browsers: ["ChromiumHeadless"],
    singleRun: true,
    karmaTypescriptConfig: {
      tsconfig: "./tsconfig.json",
    },
  });
};

注意,这是一个通用项目配置。有些开发者偏好将测试文件集中于 test 文件夹。而我比较喜欢将单元测试文件放在 src 文件夹,而 test 文件夹用于整合测试。

主要用于初始化 Enzyme 的文件 setupTests.ts 是通用的,不需要做修改。但是要注意它必须包含在 Karma 配置的 files 范围内,才可生效。

迁移单测文件

断言 Assertion

Jest 和 Chai 的断言函数有所不同,但是能一一对应。可以使用 VS Code 的批量查找替换功能,将 .toBe() 替换为 .to.equal(),将 .toEqual() 替换为 .to.deep.equal()

快照 Snapshot

如果你需要 Jest 的 Snapshot 功能,则需要安装 chai-jest-snapshot 包,并更改对应的语法。

Mock

在大多数地方,你可以依赖原生浏览器特性,而将 Jest 的 Mock 直接移除。

运行性能对比

在网上有一些文章说 Jest 的运行速度比 Karma 快。但我实际的体验于此恰好相反,为了证实这个问题,我特意做了一个对照实验。

以下测试结果运行于一台 ThinkPad T480 上。CPU:Intel Core i7 8550U,RAM:32GB DDR4。

24 个测试

Karma 系Jest 系
启动&加载时间2.9s0.5s
编译&执行时间6.5s10.5s
总耗时9.4s11.0s
两种单元测试工具集性能的比较一

在测试量不大的情况下,Karma 虽然更快一些,但是差距不甚明显。Jest 方案明显更轻量化,启动时间更快。由于两者都采用 TypeScript 编译器,推测编译时间接近。Karma 方案的执行速度明显更快,使得整体上仍然比 Jest 快。

24×8 个测试

Karma 系Jest 系
启动&加载时间4.7s0.6s
编译&执行时间13.8s27.4s
总耗时18.5s28.0s
两种单元测试工具集性能的比较二

当测试数量加倍后,Karma 的性能优势更加明显。这种优势主要来自于它并发执行测试的能力。对于实际工程,单元测试可能高达数千个,这种高并发执行优势是意义重大的。

开发效能

选择一个工具最关键的问题是:它能在多大程度上提高开发者的效能?

《 “单元测试:从 Jest 到 Karma+Mocha+Chai” 》 有 4 条评论

回复 梦中人 取消回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据