环境准备 需要用到的包
node 18.16.0# Javascript 代码编辑"@babel/core": "^7.24.7","@babel/preset-env": "^7.24.7","babel-loader": "^9.1.3",# 打包时使用的 module, 给代码中注入新的方法# https://v4.webpack.js.org/loaders/istanbul-instrumenter-loader/"istanbul-instrumenter-loader": "^3.0.1",# 数据收集工具"nyc": "^17.0.0",# 打包工具"webpack": "^5.92.0","webpack-cli": "^5.1.4"
如何使用
1. 创建一个简单的js项目,项目结构如下
├── src │ └── index.js ├── .nycrc ├── .babelrc ├── package.json ├── package-lock.json └── webpack.config.js
2. 每个文件里都有什么,怎么使用
index.jsfunction sum (a, b) {return a + b; } function reduce (a, b) {return a - b; }// 手动调用 sum 函数来生成覆盖率 sum(1, 2);
.nycrc 配置 nyc 获取哪些文件的覆盖率 和 数据输出 位置
{"include": ["src"],"exclude": ["dist"],"reporter": ["html", "text"],"all": true,"report-dir": "./coverage" }
package.json
{"build": "webpack",// 使用webpack 将index.js 打包成 dist 文件"start": "node ./dist/bundle.js",// 执行打包好的文件"coverage": "npm run build && nyc npm run start",// 使用 nyc 执行 打包好的文件并抓取数据"report:html": "nyc report --reporter=html"// 使用 nyc 生成 测试报告 }
webpack.config.js
const path = require('path'); module.exports = { mode: 'development', entry: './src/index.js', output: {path: path.resolve(__dirname, 'dist'),filename: 'bundle.js' }, module: {rules: [// 给 webpack 添加 istanbul-instrumenter-loader module// https://v4.webpack.js.org/loaders/istanbul-instrumenter-loader/{test: /\.js$/,exclude: /node_modules|\.test\.js$/,include: path.resolve(__dirname, 'src'),enforce: 'post',use: {loader: 'istanbul-instrumenter-loader',options: { esModules: true }}}] }, devtool: 'inline-source-map' };
3. 生成coverage 报告
通过上面的配置一个简单的 coverage demo 已经算是完成了
执行 npm run coverage 即可在控制台看到报告的输出
从图中可以看到 index.js 覆盖率为 66.66%如何找到没有被覆盖的代码,可以执行 npm run report:html 生成更为详细的报告,这个命令使用 nyc report --reporter=html 读取 .nyc_output 抓取的测试输出结果 json 生成html 文件
打开 coverage 文件下的 index.html 然后点到测试的那个文件即可看到详细的代码覆盖文件
如何实现
https://v4.webpack.js.org/loaders/istanbul-instrumenter-loader/
code coverage 主要通过 webpack loaders istanbul-instrumenter-loader 来实现的
通过观察不使用和使用这个 loaders 的打包文件可以发现,使用 istanbul-instrumenter-loader 打包后的文件,会被注入一个非常大的 function 将当前文件里所有的function 判断 和 变量都做上了编号
{"path": "/Users/********/Desktop/coverage1/src/index.js","statementMap": {"0": { start: { line: 2, column: 4 }, end: { line: 2, column: 17 } },},"fnMap": {"0": { name: "sum", decl: { start: { line: 1, column: 9 }, end: { line: 1, column: 12 } }, loc: { start: { line: 1, column: 20 }, end: { line: 3, column: 1 } }, line: 1 }},"branchMap": { '0': { loc: { start: { line: 8, column: 0 }, end: { line: 10, column: 1 } }, type: 'if', locations: [{ start: { line: 8, column: 0 }, end: { line: 10, column: 1 } }, { start: { line: 8, column: 0 }, end: { line: 10, column: 1 } }], line: 8 }},"s": { '0': 0, '1': 0, '2': 0, '3': 0, '4': 0 }, "f": { '0': 0, '1': 0 }, "b": { '0': [0, 0] } }function sum (a, b) { cov_29gy9r9jfk.f[0]++; cov_29gy9r9jfk.s[0]++; return a + b; } // statementMap: 记录定义变量的开始结束位置 // fnMap: 记录 Function 的 开始结束为止, function name // branchMap: 记录判断的 开始结束为止 // s,f,b: 调用方法的时候调用封装的大方法然后给对应的变量 ++ 记录,调用次数
编译过后的代码如何映射在原始文件中
source-map 中都有什么
{"version": 3,// 当前使用 source-map 的版本"file": "bundle.js",// 编译后的文件名"mappings": ";;;;;AAAA;AACA;AACA;AACA;AACA;AACA",// 这是最重要的内容,表示了源代码及编译后代码的关系"sources": [// 源文件名"webpack://manual/./src/index.js"],"sourcesContent": [// 转换前的的代码"function sum (a, b) {\r\n return a + b;\r\n}\r\nsum(1, 2);\r\n\r\n\r\n"],"names": [],// 转换前的变量和属性名称"sourceRoot": ""// 所有的sources相对的根目录 }
source-map 如何对应到 源文件中的位置
这里需要用到上面的 mappings
首先 mappings 的内容其实是 Base64 VLQ 的编码表示。
内容由三部分组成,分别为:
- 英文,表示源码及压缩代码的位置关联
- 逗号,分隔一行代码中的内容。比如说 console.log(a) 就由 console 、log 及 a 三部分组成,所以存在两个逗号。
- 分号,代表换行
所以 mappings 中的每一个字母都代表每个代码对应的位置,下面是当前 mappings 的解析结果
https://www.murzwin.com/base64vlq.html