monocart-coverage-reports v2.12.3
Monocart Coverage Reports
🌐 English | 简体中文
- 用法
- 选项配置
- 所有支持的报告类型
- 比较两种报告
- 如何收集Istanbul覆盖率数据
- 如何收集V8覆盖率数据
- 过滤V8覆盖率数据
- 使用
sourcePath
修改源文件路径 - 为未测试的文件添加空的覆盖率报告
- onEnd回调函数
- 如何忽略未覆盖的代码
- 多进程支持
- 如何使用CLI命令行
- 如何加载配置文件
- 如何合并覆盖率报告
- 常见问题
- 如何调试覆盖率数据和查看sourcemap
- 如何跟其他框架集成
- 集成的例子
- Contributing
- 更新日志
- 感谢
Usage
推荐使用 Node.js 20+.
- 安装
npm install monocart-coverage-reports
- API
const MCR = require('monocart-coverage-reports'); const mcr = MCR({ name: 'My Coverage Report - 2024-02-28', outputDir: './coverage-reports', reports: ["v8", "console-details"], cleanCache: true }); await mcr.add(coverageData); await mcr.generate();
也可以使用ESM的
import
然后加载配置文件import { CoverageReport } from 'monocart-coverage-reports'; const mcr = new CoverageReport(); await mcr.loadConfig();
参见 多进程支持
- CLI
mcr node my-app.js -r v8,console-details
参见 命令行
Options
- 默认选项: lib/default/options.js
- 选项的类型描述,见
CoverageReportOptions
lib/index.d.ts - 配置文件
Available Reports
内置V8报告(仅V8格式数据支持):
v8
v8-json
- 保存
CoverageResults
到一个json文件 (默认是coverage-report.json
) - 用于VSCode扩展来显示原生V8代码覆盖率: Monocart Coverage for VSCode
- 保存
内置Istanbul报告 (V8和Istanbul格式数据都支持):
clover
cobertura
html
html-spa
json
json-summary
lcov
lcovonly
none
teamcity
text
text-lcov
text-summary
其他内置报告 (V8和Istanbul格式数据都支持):
codacy
保存覆盖率数据到 Codacy 专属的json文件 (默认是codacy.json
)console-summary
在控制台显示覆盖率概要
console-details
在控制台显示每个文件的覆盖率概要。如果是Github actions,可以使用环境变量FORCE_COLOR: true
来强制开启颜色支持
markdown-summary
保存概要信息到markdown文件 (默认是coverage-summary.md
)。 如果是Github actions, 可以把markdown的内容添加到a job summary
cat path-to/coverage-summary.md >> $GITHUB_STEP_SUMMARY
markdown-details
保存覆盖率详情到markdown文件 (默认是coverage-details.md
)- 预览运行结果 runs
raw
只是保存原始覆盖率数据, 用于使用inputDir
参数来导入多个原始数据进行合并报告。参见 合并覆盖率报告自定义报告
{ reports: [ [path.resolve('./test/custom-istanbul-reporter.js'), { type: 'istanbul', file: 'custom-istanbul-coverage.text' }], [path.resolve('./test/custom-v8-reporter.js'), { type: 'v8', outputFile: 'custom-v8-coverage.json' }], [path.resolve('./test/custom-v8-reporter.mjs'), { type: 'both' }] ] }
- Istanbul自定义报告
例子: ./test/custom-istanbul-reporter.js, see istanbul built-in reporters' implementation for reference.
- V8自定义报告
- Istanbul自定义报告
Multiple Reports:
如何配置多个报告
const MCR = require('monocart-coverage-reports');
const coverageOptions = {
outputDir: './coverage-reports',
reports: [
// build-in reports
['console-summary'],
['v8'],
['html', {
subdir: 'istanbul'
}],
['json', {
file: 'my-json-file.json'
}],
'lcovonly',
// custom reports
// Specify reporter name with the NPM package
["custom-reporter-1"],
["custom-reporter-2", {
type: "istanbul",
key: "value"
}],
// Specify reporter name with local path
['/absolute/path/to/custom-reporter.js']
]
}
const mcr = MCR(coverageOptions);
Compare Reports
如果是V8数据格式使用Istanbul的报告,将自动从V8转换到Istanbul
Istanbul | V8 | V8 to Istanbul | |
---|---|---|---|
数据格式 | Istanbul (Object) | V8 (Array) | V8 (Array) |
输出报告 | Istanbul reports | V8 reports | Istanbul reports |
- Bytes 字节覆盖率 | ❌ | ✅ | ❌ |
- Statements 语句覆盖率 | ✅ | ✅ | ✅ |
- Branches 分支覆盖率 | ✅ | ✅ | ✅ |
- Functions 函数覆盖率 | ✅ | ✅ | ✅ |
- Lines 行覆盖率 | ✅ | ✅ | ✅ |
- Execution counts 函数执行数 | ✅ | ✅ | ✅ |
CSS 覆盖率 | ❌ | ✅ | ✅ |
压缩过的代码 | ❌ | ✅ | ❌ |
Collecting Istanbul Coverage Data
在收集Istanbul覆盖率数据之前,需要编译源代码来安装Istanbul计数器
- webpack babel-loader: babel-plugin-istanbul, 参见例子: webpack.config-istanbul.js
- 官方CLI: nyc instrument 或API: istanbul-lib-instrument
- vite: vite-plugin-istanbul
- rollup: rollup-plugin-istanbul
- swc: swc-plugin-coverage-instrument
从浏览器
- Istanbul的覆盖率数据会保存到全局的
window.__coverage__
,直接读取即可, 参见例子: test-istanbul.js
- Istanbul的覆盖率数据会保存到全局的
从Node.js
- 同理对于Node.js会保存到全局的
global.__coverage__
- 同理对于Node.js会保存到全局的
使用CDP
getIstanbulCoverage()
参见CDPClient
API
Collecting V8 Coverage Data
在收集V8覆盖率数据之前,需要开启构建工具的
sourcemap
支持,并且不要压缩代码- webpack:
devtool: source-map
andmode: development
, example webpack.config-v8.js - rollup:
sourcemap: true
andtreeshake: false
- esbuild:
sourcemap: true
,treeShaking: false
andminify: false
- vite:
sourcemap: true
andminify: false
- webpack:
浏览器 (仅支持基于Chromium的浏览器)
从Node.js
使用CDP
Collecting V8 Coverage Data with Playwright
使用Playwright的覆盖接口收集覆盖率数据
await Promise.all([
page.coverage.startJSCoverage({
// reportAnonymousScripts: true,
resetOnNavigation: false
}),
page.coverage.startCSSCoverage({
// Note, anonymous styles (without sourceURLs) are not supported, alternatively, you can use CDPClient
resetOnNavigation: false
})
]);
await page.goto("your page url");
const [jsCoverage, cssCoverage] = await Promise.all([
page.coverage.stopJSCoverage(),
page.coverage.stopCSSCoverage()
]);
const coverageData = [... jsCoverage, ... cssCoverage];
使用 @playwright/test
的 Automatic fixtures
收集覆盖率数据, 见例子: fixtures.ts
参见例子 ./test/test-v8.js, and anonymous, css
Collecting Raw V8 Coverage Data with Puppeteer
使用Puppeteer的覆盖接口收集覆盖率数据,注意Puppeteer默认不会提供原生V8的覆盖率数据,需要设置includeRawScriptCoverage
await Promise.all([
page.coverage.startJSCoverage({
// reportAnonymousScripts: true,
resetOnNavigation: false,
// provide raw v8 coverage data
includeRawScriptCoverage: true
}),
page.coverage.startCSSCoverage({
resetOnNavigation: false
})
]);
await page.goto("your page url");
const [jsCoverage, cssCoverage] = await Promise.all([
page.coverage.stopJSCoverage(),
page.coverage.stopCSSCoverage()
]);
// to raw V8 script coverage
const coverageData = [... jsCoverage.map((it) => {
return {
source: it.text,
... it.rawScriptCoverage
};
}), ... cssCoverage];
Collecting V8 Coverage Data from Node.js
有多种方法可以从Node.js收集V8覆盖率数据:
NODE_V8_COVERAGE=
dir
- 使用Node.js环境变量
NODE_V8_COVERAGE
=dir
来启动程序, 然后在进程正常结束之后,覆盖率数据将自动保存到指定的dir
目录. - 从
dir
目录读取所有的JSON文件,来生成覆盖率报告 - 参见例子:
cross-env NODE_V8_COVERAGE=
.temp/v8-coverage-env
node ./test/test-node-env.js && node ./test/generate-report.js
- 使用Node.js环境变量
V8 API + NODE_V8_COVERAGE
- 如果进程不能正常结束,比如被强制关闭,或者压根就不结束,比如启动了一个服务类的,那么需要手动写入覆盖率数据,这里需要调用接口
v8.takeCoverage()
- 参见例子:
cross-env NODE_V8_COVERAGE=
.temp/v8-coverage-api
node ./test/test-node-api.js
- 如果进程不能正常结束,比如被强制关闭,或者压根就不结束,比如启动了一个服务类的,那么需要手动写入覆盖率数据,这里需要调用接口
Inspector API
- 首先连接到Node.js的V8 inspector
- 然后使用inspector的覆盖相关API来开启和收集覆盖率数据
- 参见例子:
vm的例子 (注意这里需要使用
scriptOffset
,因为vm里一般都会加一层包裹代码,需要这个偏移位置来修正覆盖率数据块的位置):
CDP API
- 开启Node调试
- 使用CDP的覆盖率接口开启和收集覆盖率数据
- 参见例子:
node --inspect=9229 ./test/test-node-cdp.js
Node Debugging + CDP + NODE_V8_COVERAGE + V8 API
- 如果启动了一个Node服务,可以手动调用
v8.takeCoverage()
接口来保存覆盖率数据,开启Node调试就可以远程通过CDP连接的Runtime.evaluate
,来调用这个接口. - 参见koa的例子:
- 如果启动了一个Node服务,可以手动调用
Child Process + NODE_V8_COVERAGE
- 如果是子进程,可参见 命令行
Collecting V8 Coverage Data with CDPClient
API
CDPClient
为MCR
提供的内置接口类,用来更便捷的处理覆盖率相关数据,所有的API如下
// 开始和停止并收集JS的覆盖率数据
startJSCoverage: () => Promise<void>;
stopJSCoverage: () => Promise<V8CoverageEntry[]>;
// 开始和停止并收集CSS的覆盖率数据,支持匿名文件(比如style里的css)
startCSSCoverage: () => Promise<void>;
stopCSSCoverage: () => Promise<V8CoverageEntry[]>;
// 开始和停止并收集JS和CSS的覆盖率数据
startCoverage: () => Promise<void>;
stopCoverage: () => Promise<V8CoverageEntry[]>;
/** 如果开启了NODE_V8_COVERAGE,这个接口用来手动保存当前覆盖率数据 */
writeCoverage: () => Promise<string>;
/** 收集istanbul覆盖率数据 */
getIstanbulCoverage: (coverageKey?: string) => Promise<any>;
- 结合使用Node调试端口
--inspect=9229
或者浏览器调试端口--remote-debugging-port=9229
const MCR = require('monocart-coverage-reports');
const client = await MCR.CDPClient({
port: 9229
});
await client.startJSCoverage();
// run your test here
const coverageData = await client.stopJSCoverage();
const { chromium } = require('playwright');
const MCR = require('monocart-coverage-reports');
const browser = await chromium.launch();
const page = await browser.newPage();
const session = await page.context().newCDPSession(page);
const client = await MCR.CDPClient({
session
});
// both js and css coverage
await client.startCoverage();
// run your test page here
await page.goto("your page url");
const coverageData = await client.stopCoverage();
- 结合使用 Puppeteer CDPSession
const puppeteer = require('puppeteer');
const MCR = require('monocart-coverage-reports');
const browser = await puppeteer.launch({});
const page = await browser.newPage();
const session = await page.target().createCDPSession();
const client = await MCR.CDPClient({
session
});
// both js and css coverage
await client.startCoverage();
// run your test page here
await page.goto("your page url");
const coverageData = await client.stopCoverage();
- 结合使用 Selenium Webdriver WebSocket (仅支持Chrome/Edge浏览器)
const { Builder, Browser } = require('selenium-webdriver');
const MCR = require('monocart-coverage-reports');
const driver = await new Builder().forBrowser(Browser.CHROME).build();
const pageCdpConnection = await driver.createCDPConnection('page');
const session = new MCR.WSSession(pageCdpConnection._wsConnection);
const client = await MCR.CDPClient({
session
})
V8 Coverage Data API
- JavaScript V8代码覆盖官方说明
- Playwright的覆盖率接口
- Puppeteer的覆盖率接口
- DevTools Protocol的覆盖率接口 参见 ScriptCoverage 和 v8-coverage
// Coverage data for a source range.
export interface CoverageRange {
// JavaScript script source offset for the range start.
startOffset: integer;
// JavaScript script source offset for the range end.
endOffset: integer;
// Collected execution count of the source range.
count: integer;
}
// Coverage data for a JavaScript function.
/**
* @functionName can be an empty string.
* @ranges is always non-empty. The first range is called the "root range".
* @isBlockCoverage indicates if the function has block coverage information.
If this is false, it usually means that the functions was never called.
It seems to be equivalent to ranges.length === 1 && ranges[0].count === 0.
*/
export interface FunctionCoverage {
// JavaScript function name.
functionName: string;
// Source ranges inside the function with coverage data.
ranges: CoverageRange[];
// Whether coverage data for this function has block granularity.
isBlockCoverage: boolean;
}
// Coverage data for a JavaScript script.
export interface ScriptCoverage {
// JavaScript script id.
scriptId: Runtime.ScriptId;
// JavaScript script name or url.
url: string;
// Functions contained in the script that has coverage data.
functions: FunctionCoverage[];
}
export type V8CoverageData = ScriptCoverage[];
JavaScript Runtime | V8 Coverage | |
---|---|---|
Chrome (65%) | ✅ | Chromium-based |
Safari (18%) | ❌ | |
Edge (5%) | ✅ | Chromium-based |
Firefox (2%) | ❌ | |
Node.js | ✅ | |
Deno | ❌ | issue |
Bun | ❌ |
Filtering Results
Using entryFilter
and sourceFilter
to filter the results for V8 report
当收集到V8的覆盖数据时,它实际上包含了所有的入口文件的覆盖率数据, 比如有以下3个文件:
- dist/main.js
- dist/vendor.js
- dist/something-else.js
这个时候可以使用entryFilter
来过滤这些入口文件. 比如我们不需要看到vendor.js
和something-else.js
的覆盖率,就可以过滤掉,只剩下1个文件
- dist/main.js
如果一个入口文件存在行内或者链接的sourcemap文件,那么我们会尝试读取并解析sourcemap,以获取入口文件包含的所有源文件,并添加到列表。此时如果logging
没有设置成debug
,那么这个入口文件在成功解出源文件后会被移除
- src/index.js
- src/components/app.js
- node_modules/dependency/dist/dependency.js
这个时候可以使用sourceFilter
来过滤这些源文件。比如我们不需要看到源文件dependency.js
的覆盖率,就可以过滤掉,最后只剩下如下文件
- src/index.js
- src/components/app.js
过滤可以使用函数:
const coverageOptions = {
entryFilter: (entry) => entry.url.indexOf("main.js") !== -1,
sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
};
也可以使用便捷的minimatch
来匹配(推荐):
const coverageOptions = {
entryFilter: "**/main.js",
sourceFilter: "**/src/**"
};
支持多个匹配:
const coverageOptions = {
entryFilter: {
'**/node_modules/**': false,
'**/vendor.js': false,
'**/src/**': true
},
sourceFilter: {
'**/node_modules/**': false,
'**/**': true
}
};
作为CLI参数(JSON字符串,Added in: v2.8):
mcr --sourceFilter "{'**/node_modules/**':false,'**/**':true}"
注意,这些匹配实际上会转换成一个过滤函数(如下),所以如果一个匹配成功则会直接返回,后面的将不再继续匹配。请注意先后顺序,如果存在包含关系的,可以调整上下顺序,最后如果都未匹配,则默认返回false
const coverageOptions = {
entryFilter: (entry) => {
if (minimatch(entry.url, '**/node_modules/**')) { return false; }
if (minimatch(entry.url, '**/vendor.js')) { return false; }
if (minimatch(entry.url, '**/src/**')) { return true; }
return false; // else unmatched
}
};
Using filter
instead of entryFilter
and sourceFilter
如果你不想定义两个过滤器,可以使用 filter
选项代替,可以将多个匹配合并在一起. (Added in: v2.8)
const coverageOptions = {
// combined patterns
filter: {
'**/node_modules/**': false,
'**/vendor.js': false,
'**/src/**': true
'**/**': true
}
};
Resolve sourcePath
for the Source Files
当一个文件从sourcemap解包,它的路径可能是个虚拟路径, 此时可以使用sourcePath
选项来修改文件路径。比如,我们测试了多个dist包的入口文件,它们的源文件可能包含了一些共同的文件,但路径可能不同,如果我们需要相同的文件覆盖率数据可以自动合并,那么需要使用sourcePath
来统一这些相同文件的路径
const coverageOptions = {
sourcePath: (filePath) => {
// Remove the virtual prefix
const list = ['my-dist-file1/', 'my-dist-file2/'];
for (const str of list) {
if (filePath.startsWith(str)) {
return filePath.slice(str.length);
}
}
return filePath;
}
};
它也支持简单key/value的替换:
const coverageOptions = {
sourcePath: {
'my-dist-file1/': '',
'my-dist-file2/': ''
}
};
解决文件路径不完整的问题:
const path = require("path")
// MCR coverage options
const coverageOptions = {
sourcePath: (filePath, info)=> {
if (!filePath.includes('/') && info.distFile) {
return `${path.dirname(info.distFile)}/${filePath}`;
}
return filePath;
}
}
Adding Empty Coverage for Untested Files
默认,未测试的文件是不会包含到覆盖率报告的,需要使用all
选项来为这些文件添加一个空的覆盖率,也就是0%
const coverageOptions = {
all: './src',
// 支持多个目录
all: ['./src', './lib'],
};
未测试的文件也适用于sourceFilter
过滤器. 而且也可以指定自己的filter
过滤器 (可以返回文件类型来支持js或css的覆盖率格式):
const coverageOptions = {
all: {
dir: ['./src'],
filter: {
// exclude files
'**/ignored-*.js': false,
'**/*.html': false,
// empty css coverage
'**/*.scss': "css",
'**/*': true
}
}
};
我们可能需要编译.ts, .jsx, .vue等等这样的文件, 这样才能被默认的AST解析器解析,以得到更多的覆盖率指标的数据
const path = require("path");
const swc = require("@swc/core");
const coverageOptions = {
all: {
dir: ['./src'],
transformer: async (entry) => {
const { code, map } = await swc.transform(entry.source, {
filename: path.basename(entry.url),
sourceMaps: true,
isModule: true,
jsc: {
parser: {
syntax: "typescript",
jsx: true
},
transform: {}
}
});
entry.source = code;
entry.sourceMap = JSON.parse(map);
}
}
};
onEnd Hook
结束回调可以用来自定义业务需求,比如检测覆盖率是否达标,对比每个指标的thresholds,如果低于要求的值则可以抛出一个错误退出
const EC = require('eight-colors');
const coverageOptions = {
name: 'My Coverage Report',
outputDir: './coverage-reports',
onEnd: (coverageResults) => {
const thresholds = {
bytes: 80,
lines: 60
};
console.log('check thresholds ...', thresholds);
const errors = [];
const { summary } = coverageResults;
Object.keys(thresholds).forEach((k) => {
const pct = summary[k].pct;
if (pct < thresholds[k]) {
errors.push(`Coverage threshold for ${k} (${pct} %) not met: ${thresholds[k]} %`);
}
});
if (errors.length) {
const errMsg = errors.join('\n');
console.log(EC.red(errMsg));
// throw new Error(errMsg);
// process.exit(1);
}
}
}
Ignoring Uncovered Codes
使用特定的注释,以v8 ignore
开头可以忽略未覆盖的代码:
- 忽略开始到结束
/* v8 ignore start */
function uncovered() {
}
/* v8 ignore stop */
- 忽略接下来一行或者多行
/* v8 ignore next */
const os = platform === 'wind32' ? 'Windows' : 'Other';
const os = platform === 'wind32' ? 'Windows' /* v8 ignore next */ : 'Other';
// v8 ignore next 3
if (platform === 'linux') {
console.log('hello linux');
}
- 兼容支持 c8 coverage 或 nodejs coverage 的语法格式
/* c8 ignore start */
function uncovered() {
}
/* c8 ignore stop */
/* node:coverage disable */
function uncovered() {
}
/* node:coverage enable */
Multiprocessing Support
多进程支持可以很好的解决异步并行的情况。所有的覆盖率数据会保存到
[outputDir]/.cache
,在报告生成之后,这些缓存数据会被清除。除非开启了调试模式,或者使用了raw
报告
- 主进程,初始化,清理之前的缓存
const MCR = require('monocart-coverage-reports'); const coverageOptions = require('path-to/same-options.js'); const mcr = MCR(coverageOptions); // clean previous cache before the start of testing // unless the running environment is new and no cache mcr.cleanCache();
- 子进程1, 测试业务1
const MCR = require('monocart-coverage-reports');
const coverageOptions = require('path-to/same-options.js');
const mcr = MCR(coverageOptions);
await mcr.add(coverageData1);
- 子进程2, 测试业务2
const MCR = require('monocart-coverage-reports');
const coverageOptions = require('path-to/same-options.js');
const mcr = MCR(coverageOptions);
await mcr.add(coverageData2);
- 主进程,所有测试完成之后
// generate coverage reports after the completion of testing
const MCR = require('monocart-coverage-reports');
const coverageOptions = require('path-to/same-options.js');
const mcr = MCR(coverageOptions);
await mcr.generate();
Command Line
使用
mcr
命令行将使用NODE_V8_COVERAGE=dir
来启动一个子进程运行程序,直到正常退出,然后自动从dir
目录来读取覆盖率数据,并生成覆盖率报告
- 全局安装
npm i monocart-coverage-reports -g
mcr node ./test/specs/node.test.js -r v8,console-details --lcov
- 本地项目安装
npm i monocart-coverage-reports
npx mcr node ./test/specs/node.test.js -r v8,console-details --lcov
命令行参数 直接运行
mcr
或mcr --help
查看所有CLI的参数使用
--
可以隔离子程序参数,以免两种参数混淆
mcr -c mcr.config.js -- sub-cli -c sub-cli.config.js
Config File
根据以下优先级加载配置文件
- 自定义配置文件(如果没有指定则加载后面的默认配置文件):
- CLI:
mcr --config <my-config-file-path>
- API:
await mcr.loadConfig("my-config-file-path")
- CLI:
mcr.config.js
mcr.config.cjs
mcr.config.mjs
mcr.config.json
- json formatmcr.config.ts
(requires preloading the ts execution module)
Merge Coverage Reports
以下这些使用场景可能需要使用合并覆盖率报告:
- 多个执行环境,比如Node.js服务端,以及浏览器客户端,比如
Next.js
- 多种测试类型,比如
Jest
单元测试,以及Playwright
的端到端自动化测试 - 分布式测试,测试结果保存到了多台机器或不同的容器中
Automatic Merging
- 默认
MCR
在执行generate()
时会自动合并覆盖率数据。所以可以在多进程支持下,多次添加覆盖率数据,最后将自动合并 - 比如
Next.js
就可以同时添加前后端覆盖率数据,最后再执行generate()
生成覆盖率报告,见例子nextjs-with-playwright - 使用
Codecov
在线覆盖率报告服务,请设置输出codecov
报告, 它会生成专属的codecov.json
,如果有多个codecov.json
文件上传,它们会自动合并数据,参见Codecov 和 合并报告说明
Manual Merging
手动合并覆盖率报告需要使用raw
报告来导出原始的覆盖率数据到指定的目录
- 比如,单元测试保存到
./coverage-reports/unit/raw
,见例子Jest
+ jest-monocart-coverageVitest
+ vitest-monocart-coverage
const coverageOptions = {
name: 'My Unit Test Coverage Report',
outputDir: "./coverage-reports/unit",
reports: [
['raw', {
// relative path will be "./coverage-reports/unit/raw"
// defaults to raw
outputDir: "raw"
}],
['v8'],
['console-details']
]
};
同样的,E2E测试保存到
./coverage-reports/e2e/raw
. 见例子:Playwright
+ monocart-reporter with coverage APIPlaywright
+MCR
, see playwright-coverage- see more Integration Examples
然后创建一个
merge-coverage.js
文件,使用inputDir
参数导入raw
数据,来生成合并的覆盖率报告.
// merge-coverage.js
const fs = require('fs');
const { CoverageReport } = require('monocart-coverage-reports');
const inputDir = [
'./coverage-reports/unit/raw',
'./coverage-reports/e2e/raw'
];
const coverageOptions = {
name: 'My Merged Coverage Report',
inputDir,
outputDir: './coverage-reports/merged',
// filter for both unit and e2e
entryFilter: {
'**/node_modules/**': false,
'**/*': true
},
sourceFilter: {
'**/node_modules/**': false,
'**/src/**': true
},
sourcePath: (filePath, info) => {
// Unify the file path for the same files
// For example, the file index.js has different paths:
// unit: unit-dist/src/index.js
// e2e: e2e-dist/src/index.js
// return filePath.replace("unit-dist/", "").replace("e2e-dist/", "")
return filePath;
},
reports: [
['v8'],
['console-details']
],
onEnd: () => {
// remove the raw files if it useless
// inputDir.forEach((p) => {
// fs.rmSync(p, {
// recursive: true,
// force: true
// });
// });
}
};
await new CoverageReport(coverageOptions).generate();
- 最后在所有测试完成后运行
node path/to/merge-coverage.js
. 所有的执行脚本大概如下:
{
"scripts": {
"test:unit": "jest",
"test:e2e": "playwright test",
"merge-coverage": "node path/to/merge-coverage.js",
"test": "npm run test:unit && npm run test:e2e && npm run merge-coverage"
}
}
参见例子: merge-code-coverage
Common issues
常见问题
Unexpected coverage
覆盖率看起来不正确,多数情况是因为sourcemap转换的问题导致的. 可以先尝试设置构建工具的
minify=false
也就是不要压缩代码来解决。下面来看看sourcemap存在问题的具体原因:const a = tf ? 'true' : 'false'; ^ ^ ^ m1 p m2
上面是经过构建工具编译过的代码,通过AST分析,位置
p
对应的原始位置是我们要找的,而从sourcemap里仅能找到离p
最近的位置映射m1
和m2
,也就是位置p
并没有精确的映射保存到sourcemap里,从而无法直接获取精确的原始位置,但我们能知道p
的原始位置应该在m1
和m2
之间。
MCR
如何解决这个问题:
- 1, 首先会尝试使用
diff-sequences
工具来比较m1
和m2
之间的生成代码和原始代码,找到p
对应的字符位置,可以解决绝大多数问题。但是如果代码是非JS格式的,比如Vue模板是HTML,或JSX这些,不管怎么比较也是很难精确找到对应位置的,甚至此时的sourcemap本身都比较乱。 - 2, 然后就是通过分析AST,找到所有的functions, statements 和 branches,因为V8覆盖率本身不提供这些指标的覆盖率. (对于分支覆盖暂不支持
AssignmentPattern
类型,因为即使分析AST也无法从V8覆盖率找到它的数据)。
Unparsable source
源码无法解析问题。由上面我们知道MCR
通过分析源码的AST获取更多指标的覆盖率信息,但源码如果不是标准的 ECMAScript,比如ts
, jsx
这些,那么分析的时候就会报错,此时我们可以手动来编译这些文件(可行但不推荐).
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import * as TsNode from 'ts-node';
const coverageOptions = {
onEntry: async (entry) => {
const filePath = fileURLToPath(entry.url)
const originalSource = fs.readFileSync(filePath).toString("utf-8");
const fileName = path.basename(filePath);
const tn = TsNode.create({});
const source = tn.compile(originalSource, fileName);
entry.fake = false;
entry.source = source;
}
}
JavaScript heap out of memory
内存溢出问题可能出现在有太多的原生V8覆盖率文件要处理. 我们可以使用Node.js的一个选项来增加内存使用:
- run: npm run test:coverage
env:
NODE_OPTIONS: --max-old-space-size=8192
Debug for Coverage and Sourcemap
当你觉得覆盖率存在问题的时候,
MCR
支持自行调试来核验覆盖率的准确性
- 首先打开调试设置
logging: 'debug'
const coverageOptions = { logging: 'debug', reports: [ ['v8'], ['console-details'] ] };
调试模式下,也就是
logging
为debug
的时候, 原始的覆盖率数据将保留在[outputDir]/.cache
缓存目录下,不会删除,如果使用了raw
报告,那么位置变为[outputDir]/raw
下,这样我们可以打开v8报告的html文件,通过下面新增的一些调试帮助信息来核对覆盖率
- 调试sourcemap可以直接使用Source Map Visualization (esbuild作者提供的sourcemap在线查看器)
- 生成额外的source和sourcemap文件到cache或raw文件夹
const coverageOptions = {
logging: 'debug',
sourceMap: true
};
- 使用环境变量
MCR_LOG_TIME
显示时间日志
process.env.MCR_LOG_TIME = true
Integration with Any Testing Framework
通用集成方案
- 通过API接口在程序集成
- 首先,要自行收集覆盖率数据,然后,添加到报告实例
await mcr.add(coverageData)
- 最后,生成覆盖率报告
await mcr.generate()
- 参见 多进程支持
- 首先,要自行收集覆盖率数据,然后,添加到报告实例
- 通过CLI命令行与其他命令行集成
- 直接在其他命令行前面添加mcr的命令行即可
mcr your-cli --your-arguments
- 参见 命令行
- 直接在其他命令行前面添加mcr的命令行即可
Integration Examples
Playwright
- playwright-coverage - Example for Playwright coverage reports
- playwright-bdd-coverage - Example for Playwright BDD coverage reports
- monocart-reporter - Playwright custom reporter, supports generating Code coverage report
- Coverage for component testing with
monocart-reporter
: - Coverage for Next.js, both server side and client side:
- Coverage for Remix:
- see Collecting V8 Coverage Data with Playwright
c8
- c8 has integrated
MCR
as an experimental feature since v10.1.0
c8 --experimental-monocart --reporter=v8 --reporter=console-details node foo.js
CodeceptJS
- CodeceptJS is a BDD + AI testing framework for e2e testing, it has integrated
MCR
since v3.5.15, see plugins/coverage
VSCode
- Monocart Coverage for VSCode - Shows native V8 code coverage in VSCode
Jest
- jest-monocart-coverage - Jest custom reporter for coverage reports
- merge-code-coverage - Example for merging code coverage (Jest unit + Playwright e2e sharding)
Vitest
- vitest-monocart-coverage - Vitest custom provider module for coverage reports
- merge-code-coverage-vitest - Example for merging code coverage (Vitest unit + Playwright e2e sharding)
Node Test Runner
- node-monocart-coverage - Custom reporter for Node test runner for coverage
Puppeteer
- jest-puppeteer-coverage - Example for Jest puppeteer coverage
- maplibre-gl-js - Example for Jest (unit) + Puppeteer (e2e) + Codecov
- see Collecting Raw V8 Coverage Data with Puppeteer
Cypress
- cypress-monocart-coverage - Cypress plugin for coverage reports
WebdriverIO
- wdio-monocart-service - WebdriverIO service for coverage reports
Storybook Test Runner
- storybook-monocart-coverage - Example for Storybook V8 coverage reports
TestCafe
- testcafe-reporter-coverage - TestCafe custom reporter for coverage reports
Selenium Webdriver
- selenium-webdriver-coverage - Example for Selenium Webdriver V8 coverage reports
Mocha
mcr mocha ./test/**/*.js
TypeScript
cross-env NODE_OPTIONS="--import tsx" npx mcr tsx ./src/example.ts
cross-env NODE_OPTIONS="--import tsx" npx mcr mocha ./test/**/*.ts
# Node.js v18.19.0 +
mcr --import tsx tsx ./src/example.ts
mcr --import tsx mocha ./test/**/*.ts
cross-env NODE_OPTIONS="--loader ts-node/esm --no-warnings" npx mcr ts-node ./src/example.ts
cross-env NODE_OPTIONS="--loader ts-node/esm --no-warnings" npx mcr mocha ./test/**/*.ts
AVA
mcr ava
Codecov
- Supports native
codecov
built-in report (specification)
const coverageOptions = {
outputDir: "./coverage-reports",
reports: [
['codecov']
]
};
- Github actions:
- name: Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage-reports/codecov.json
Codacy
- Using
lcov
report:
const coverageOptions = {
outputDir: "./coverage-reports",
lcov: true
};
- Github actions:
- name: Codacy Coverage Reporter
uses: codacy/codacy-coverage-reporter-action@v1
with:
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
coverage-reports: ./docs/mcr/lcov.info
Coveralls
- Using
lcov
report:
const coverageOptions = {
outputDir: "./coverage-reports",
lcov: true
};
- Github actions:
- name: Coveralls
uses: coverallsapp/github-action@v2
with:
files: ./coverage-reports/lcov.info
Sonar Cloud
- Using
lcov
report. Github actions example:
- name: Analyze with SonarCloud
uses: sonarsource/sonarcloud-github-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
projectBaseDir: ./
args: >
-Dsonar.organization=cenfun
-Dsonar.projectKey=monocart-coverage-reports
-Dsonar.projectName=monocart-coverage-reports
-Dsonar.javascript.lcov.reportPaths=docs/mcr/lcov.info
-Dsonar.sources=lib
-Dsonar.tests=test
-Dsonar.exclusions=dist/*,packages/*
Contributing
- Node.js 20+
- VSCode (extensions: eslint/stylelint/vue)
npm install
npx playwright install --with-deps
npm run build
npm run test
npm run dev
- Refreshing
eol=lf
for snapshot of test (Windows)
git add . -u
git commit -m "Saving files before refreshing line endings"
npm run eol
Thanks
7 months ago
8 months ago
8 months ago
9 months ago
8 months ago
6 months ago
5 months ago
6 months ago
5 months ago
10 months ago
10 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
1 year ago
1 year ago
1 year ago
12 months ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago