“Testing leads to failure, and failure leads to understanding.”
“Testing leads to failure, and failure leads to understanding.”
测试是软件开发中重要的一环,有了测试项目开发起来大脑负担减少,心里踏实,许多没想到的边界条件也能一一揪出。Karma+Mocha+Chai 是经过时间考验的经典测试组合,具有较高的灵活性,非常值得研究学习。本文将从零开始搭建,同时配合 TypeScript 进行测试。
Jest 是一个非常简单易用的测试框架,它内置了测试中各种常用功能,API 简洁清晰,只需少量配置即可快速编写测试。但同时 Jest 内部造了很多轮子,比如模块路径解析就曾踩到坑;其使用 JSDOM 模拟浏览器环境来支持并行测试,大大加快了速度,但也带来了 JSDOM 的局限性与坑;最大的问题是不能方便地在真实浏览器中测试,这个缺陷多年一直没有得到官方重视,未来支持的可能性很小。
所以,如果只需要简单地测试框架组件以及一些纯逻辑的功能,使用 Jest 无疑是最方便的;但如果涉及到 DOM 相关的一些测试,就不必费时间在 Jest 上折腾 puppeteer 了,直接上 Karma 全家桶反而更容易。
许多框架 CLI 和脚手架都可以自动配置好测试框架,为什么我们还要自行搭建?除去研究学习的原因,整个搭建起来其实也没有想象中的困难,每个工具各司其职给了我们很大的自由去挑选合适的组合。预设的方案一般很难符合每个项目的需要,像 Neutrino 脚手架就意识到这一点,Neutrino 9 虽然依然号称“零配置”,但其角色已从项目零配置转移到工具零配置上。了解工具的基本原理,即便是配置脚手架来也会更得心应手。
首先我们来了解一下将要出场的几个工具是干什么的:
npm install -D karma
要使用 Karma 只需要配置一个文件即可,理论上放哪里都行,方便起见我们在项目根目录新建 karma.config.js
。
module.exports = config => {
config.set({
singleRun: !!process.env.CI,
mime: {
'text/x-typescript': ['ts', 'tsx']
}
})
}
singleRun
:在本地我们通常希望测试跑完之后 Karma 依然继续检测文件变化重跑,但在 CI 上则必须完成结束测试,否测 CI 会一直等待 Karma 直到超时。mime
: Karma 不认识 TypeScript 文件,我们得告诉它。npm install -D karma-chrome-launcher karma-firefox-launcher
根据你的需求添加即可。
module.exports = config => {
config.set({
singleRun: !!process.env.CI,
browsers: process.env.CI ? ['Chrome', 'Firefox'] : ['Chrome'],
mime: {
'text/x-typescript': ['ts', 'tsx']
}
})
}
npm install -D mocha chai karam karma-mocha karma-chai @types/chai @types/mocha
植入全局变量就不必反复 import
。
module.exports = config => {
config.set({
singleRun: !!process.env.CI,
browsers: process.env.CI ? ['Chrome', 'Firefox'] : ['Chrome'],
frameworks: ['mocha', 'chai'],
mime: {
'text/x-typescript': ['ts', 'tsx']
}
})
}
我们的测试也将使用 TypeScript 编写,@types/Mocha 会自动添加全局类型,但 Chai 默认不会,所以我们为测试的 ts 新建一个配置 test/tscofig.json
{
"extends": "../tsconfig",
"compilerOptions": {
"typeRoots": [
"../node_modules/@types",
"./typings"
]
},
"include": [
"./**/*.spec.tsx"
]
}
然后新建 test/typings/global/index.d.ts
import * as Chai from 'chai'
declare global {
interface Window {
expect: Chai.ExpectStatic
}
var expect: Chai.ExpectStatic
}
npm install -D webpack karma-webpack ts-loader @types/webpack-env
我们将使用 karma-webpack 对 TypeScript 进行打包。
module.exports = config => {
config.set({
singleRun: !!process.env.CI,
browsers: process.env.CI ? ['Chrome', 'Firefox'] : ['Chrome'],
mime: {
'text/x-typescript': ['ts', 'tsx']
},
webpackMiddleware: {
noInfo: true,
stats: 'errors-only'
},
webpack: {
mode: 'development',
entry: './src/index.ts',
output: {
filename: '[name].js'
},
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: 'ts-loader',
options: {
configFile: 'test/tsconfig.json'
}
},
exclude: [path.join(__dirname, 'node_modules')]
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json']
}
}
})
}
注意我们这里给 ts-loader 指向了测试的 tsconfig.json。
也许你听说过 @babel/preset-typescript,它确实也可以转换 Typescript,但也仅仅是转换,由于无法进行 type-check,生成的代码还是有些不一样。如果没有特殊的需求,用回原生的编译器显然坑更少。
如果你的项目是基于 Rollup 的,重用 Rollup 的配置来加载测试似乎是个更好的选择。但也要注意 Rollup 的生态跟 Webpack 相比还是有些差距,许多 Rollup 相关的插件都疏于维护,我之前就一直没法让 rollup-plugin-istanbul 正确定位到 TypeScript 的源码。
使用 Webpack 之后我们就不需要 Karma 来匹配文件,而是交给 Webpack 处理,只留一个入口给 Karma 即可。
module.exports = config => {
config.set({
singleRun: !!process.env.CI,
browsers: process.env.CI ? ['Chrome', 'Firefox'] : ['Chrome'],
mime: {
'text/x-typescript': ['ts', 'tsx']
},
files: ['test/index.ts'],
preprocessors: { 'test/index.ts': ['webpack'] },
webpackMiddleware: {
noInfo: true,
stats: 'errors-only'
},
webpack: {
mode: 'development',
entry: './src/index.ts',
output: {
filename: '[name].js'
},
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: 'ts-loader',
options: {
configFile: 'test/tsconfig.json'
}
},
exclude: [path.join(__dirname, 'node_modules')]
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json']
}
}
})
}
同时新建 test/index.js
将所有测试以及所有需要计算覆盖率的源码都 require
进来。
const tests = require.context('./', true, /\.spec\.tsx?$/)
tests.keys().forEach(tests)
const sources = require.context('../src/', true, /\.tsx?$/)
sources.keys().forEach(sources)
npm install -D karma-nyan-reporter
接下来我们配置最后一步,汇报测试结果。这里不得不提这个萌萌的 karma-nyan-reporter
如果不希望出现动画,只开启 renderOnRunCompleteOnly
即可。
module.exports = config => {
config.set({
singleRun: !!process.env.CI,
browsers: process.env.CI ? ['Chrome', 'Firefox'] : ['Chrome'],
mime: {
'text/x-typescript': ['ts', 'tsx']
},
files: ['test/index.ts'],
preprocessors: {
'test/index.ts': ['webpack']
},
reporters: ['nyan'],
webpackMiddleware: {
noInfo: true,
stats: 'errors-only'
},
webpack: {
mode: 'development',
entry: './src/index.ts',
output: {
filename: '[name].js'
},
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: 'ts-loader',
options: {
configFile: 'test/tsconfig.json'
}
},
exclude: [path.join(__dirname, 'node_modules')]
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json']
}
},
nyanReporter: {
renderOnRunCompleteOnly: process.env.CI
}
})
}
npm install -D istanbul-instrumenter-loader karma-coverage-istanbul-reporter
整合 Istanbul 主要分两步,先在 Webpack 中配置 instrumenter 以植入计数,最后配置汇报器汇报结果。
module.exports = config => {
config.set({
singleRun: !!process.env.CI,
browsers: process.env.CI ? ['Chrome', 'Firefox'] : ['Chrome'],
mime: {
'text/x-typescript': ['ts', 'tsx']
},
files: ['test/index.ts'],
preprocessors: {
'test/index.ts': ['webpack']
},
reporters: ['nyan', 'coverage-istanbul'],
webpackMiddleware: {
noInfo: true,
stats: 'errors-only'
},
webpack: {
mode: 'development',
entry: './src/index.ts',
output: {
filename: '[name].js'
},
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: 'ts-loader',
options: {
configFile: 'test/tsconfig.json'
}
},
exclude: [path.join(__dirname, 'node_modules')]
},
{ test: /\.tsx?$/, include: [path.join(__dirname, 'src')], enforce: 'post', use: { loader: 'istanbul-instrumenter-loader', options: { esModules: true } } } ]
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json']
}
},
coverageIstanbulReporter: process.env.CI ? { reports: ['lcovonly', 'text-summary'], dir: path.join(__dirname, 'coverage'), combineBrowserReports: true, fixWebpackSourcePaths: true } : { reports: ['html', 'lcovonly', 'text-summary'], dir: path.join(__dirname, 'coverage/%browser%/'), fixWebpackSourcePaths: true, 'report-config': { html: { outdir: 'html' } } },
nyanReporter: {
renderOnRunCompleteOnly: process.env.CI
}
})
}
注意这里我们配置汇报器输出的类型,
text-summary
主要方便我们在命令行中看到统计。html
可以 coverage/
目录下浏览各个源代码的覆盖情况。lcovonly
主要为了给其它工具使用,比如接下来要配置的 Coveralls。如果你的源码中有针对浏览器特性或坑的代码,那么每个浏览器测试出来的覆盖率可能会不一样,通过 combineBrowserReports
即可合并覆盖率。
如果需要分开查看,给 dir
路径提供 /%browser%/
会自动分开生成各个浏览器的结果。
出于各种原因,有时候我们希望让 Istanbul 忽略一部分难以测试的源码。可以在代码中加入特殊的注释如 /* istanbul ignore next */
来实现,其文档列举了完整的例子。
但无论如何,最好一并附上忽略的原因,以免给阅读源码的人(包括未来的自己)造成困扰。
npm install -D karma-coveralls
接下来我们可以将覆盖率上传到各种服务中进行分析,这里我们以 Coveralls 为例。
注意我们配置了只在 CI 上上传,因为我们前面配置了 Karma 在本地会常开着不停跑测试。
module.exports = config => {
config.set({
singleRun: !!process.env.CI,
browsers: process.env.CI ? ['Chrome', 'Firefox'] : ['Chrome'],
mime: {
'text/x-typescript': ['ts', 'tsx']
},
files: ['test/index.ts'],
preprocessors: {
'test/index.ts': ['webpack']
},
reporters: process.env.CI ? ['nyan', 'coverage-istanbul', 'coveralls'] : ['nyan', 'coverage-istanbul'],
webpackMiddleware: {
noInfo: true,
stats: 'errors-only'
},
webpack: {
mode: 'development',
entry: './src/index.ts',
output: {
filename: '[name].js'
},
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: {
loader: 'ts-loader',
options: {
configFile: 'test/tsconfig.json'
}
},
exclude: [path.join(__dirname, 'node_modules')]
},
{
test: /\.tsx?$/,
include: [path.join(__dirname, 'src')],
enforce: 'post',
use: {
loader: 'istanbul-instrumenter-loader',
options: { esModules: true }
}
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json']
}
},
coverageIstanbulReporter: process.env.CI
? {
reports: ['lcovonly', 'text-summary'],
dir: path.join(__dirname, 'coverage'),
combineBrowserReports: true,
fixWebpackSourcePaths: true
}
: {
reports: ['html', 'lcovonly', 'text-summary'],
dir: path.join(__dirname, 'coverage/%browser%/'),
fixWebpackSourcePaths: true,
'report-config': {
html: { outdir: 'html' }
}
},
coverageReporter: { type: 'lcovonly', dir: 'coverage/' },
nyanReporter: {
renderOnRunCompleteOnly: process.env.CI
}
})
}
上 Coveralls 网站开启需要测试的仓库,会得到一个 TOKEN
。
在 Travis 上同样开启这个仓库,在该项目设置中 Environment Variables 下添加环境变量 COVERALLS_REPO_TOKEN
,值为上面的 Token。
Travis CI 是命令行环境,默认跑不了 GUI 程序,我们需要做些配置。
新建 .travis.yml
language: node_js
node_js:
- 10.6.0
before_install:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
addons:
firefox: latest
chrome: stable
script:
- yarn build
- yarn test
其中浏览器可以根据插件的文档选择不同版本。
最后我们把结果放到 README 上以展示这个项目的可靠性。
比如我的一个项目 get-selection-more
[![npm-version](https://img.shields.io/npm/v/get-selection-more.svg)](https://www.npmjs.com/package/get-selection-more)
[![Build Status](https://travis-ci.org/crimx/get-selection-more.svg?branch=master)](https://travis-ci.org/crimx/get-selection-more)
[![Coverage Status](https://coveralls.io/repos/github/crimx/get-selection-more/badge.svg?branch=master)](https://coveralls.io/github/crimx/get-selection-more?branch=master)
将 crimx/get-selection-more
换成你的项目名称即可。
这就是搭建 Karma 全家桶的基本流程,现在只需在 test
下新建 **/*.spec.ts
即可开始编写测试。由于每个工具各司其职,要更换或者添加新功能都非常容易。希望本文能帮助大家减少编写测试的困难,舒服地进行开发,产出更多可靠的项目。
评论没有加载,检查你的局域网
Cannot load comments. Check you network.