我所参与过的 Web 项目大部分都使用 React 框架。Jest 是我们曾经最主要使用的单元测试 (Unit Test) 框架。Jest 配置足够简单,功能也足够丰富。但是随着前端开发逐渐进入深水区,Jest 的短板开始逐渐显现。我在经过探索和尝试后,迁移到了 Karma+Mocha+Chai 的解决方案。在此分享这两种技术方案的区别和取舍,希望对遇到同样问题的同学有所帮助。
Jest 是 Facebook 开发的一体式单元测试工具。你不需要关心什么是测试运行环境,什么是测试框架,什么是断言库。Jest 就是你所需要的全部。它只需要非常少的配置,几乎做到了开箱即用。Jest 随着 React 生态的繁荣而大行其道,然而它并不是那么完美。Jest 的运行环境是 JSDOM,一个伪 DOM 引擎。
JSDOM 不能理解 DOM 的布局,尺寸,样式,以及浏览器的高级 API,例如 ResizeObserver
,matchMedia()
。我们无法在代码中避免使用它们,于是只能去 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 Runtime | Chrome/Firefox/Safari/Edge/IE | JSDOM + Node.js |
代码转译器 Transpiler | TypeScript | TypeScript |
测试运行工具 Test Runner | Karma | Jest |
测试框架 Test Framework | Mocha/Jasmine | Jest |
断言库 Assertion Library | Chai/Expect.js | Jest |
React 测试工具 React Test Utility | Enzyme/React-Testing-Library | Enzyme/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.9s | 0.5s |
编译&执行时间 | 6.5s | 10.5s |
总耗时 | 9.4s | 11.0s |
在测试量不大的情况下,Karma 虽然更快一些,但是差距不甚明显。Jest 方案明显更轻量化,启动时间更快。由于两者都采用 TypeScript 编译器,推测编译时间接近。Karma 方案的执行速度明显更快,使得整体上仍然比 Jest 快。
24×8 个测试
Karma 系 | Jest 系 | |
---|---|---|
启动&加载时间 | 4.7s | 0.6s |
编译&执行时间 | 13.8s | 27.4s |
总耗时 | 18.5s | 28.0s |
当测试数量加倍后,Karma 的性能优势更加明显。这种优势主要来自于它并发执行测试的能力。对于实际工程,单元测试可能高达数千个,这种高并发执行优势是意义重大的。
开发效能
选择一个工具最关键的问题是:它能在多大程度上提高开发者的效能?
发表回复