A code coverage tool to generate native V8 reports or Istanbul reports.
npm install monocart-coverage-reports
!license
!build


!dependencies

🌐 English | 简体中文
> JS代码覆盖率工具,用来生成原生的V8或者Istanbul代码覆盖率报告
* 用法
* 选项配置
* 所有支持的报告类型
* 比较两种报告
* 如何收集Istanbul覆盖率数据
* 如何收集V8覆盖率数据
- 用Playwright
- 用Puppeteer
- 从Node.js
- 使用CDPClientAPI
- 参考V8覆盖率的API
* 过滤V8覆盖率数据
* 使用 sourcePath 修改源文件路径
* 为未测试的文件添加空的覆盖率报告
* onEnd回调函数
* 如何忽略未覆盖的代码
* 多进程支持
* 如何使用CLI命令行
* 如何加载配置文件
* 如何合并覆盖率报告
- 自动合并
- 手动合并
* 常见问题
- Unexpected coverage
- Unparsable source
- JavaScript heap out of memory
* 如何调试覆盖率数据和查看sourcemap
* 如何跟其他框架集成
* 集成的例子
- Playwright
- c8
- CodeceptJS
- VSCode
- Jest
- Vitest
- Node Test Runner
- Puppeteer
- Cypress
- WebdriverIO
- Storybook Test Runner
- TestCafe
- Selenium Webdriver
- Mocha
- TypeScript
- AVA
- Codecov
- Codacy
- Coveralls
- Sonar Cloud
* Contributing
* 更新日志
* 感谢
sh
npm install monocart-coverage-reports
`
- API
`js
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 然后加载配置文件
`js
import { CoverageReport } from 'monocart-coverage-reports';
const mcr = new CoverageReport();
await mcr.loadConfig();
`
参见 多进程支持- CLI
`sh
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覆盖率报告界面,更好的用户体验
- 支持原生的Bytes覆盖率指标
- 支持高性能处理大数据
- 支持任何运行时代码的覆盖率(压缩后的)
- 支持CSS代码覆盖率(用于分析CSS的冗余代码)
- 对Sourcemap转换有更好的支持
- 预览: V8 and more
-
v8-json
- 保存 CoverageResults 到一个json文件 (默认是 coverage-report.json)
- 用于VSCode扩展来显示原生V8代码覆盖率: Monocart Coverage for VSCode
> 内置Istanbul报告 (V8和Istanbul格式数据都支持):
-
clover
- cobertura
- html
- Istanbul html
- V8 to Istanbul
- html-spa
- json
- json-summary
- lcov
- lcovonly
- V8 lcov.info
- Istanbul lcov.info
- none
- teamcity
- text
- text-lcov
- text-summary> 其他内置报告 (V8和Istanbul格式数据都支持):
-
codecov 保存覆盖率数据到 Codecov 专属的json文件 (默认是codecov.json), 见例子 -
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
`sh
cat path-to/coverage-summary.md >> $GITHUB_STEP_SUMMARY
`
-
markdown-details 保存覆盖率详情到markdown文件 (默认是 coverage-details.md)
- 预览运行结果 runs-
raw 只是保存原始覆盖率数据, 用于使用inputDir参数来导入多个原始数据进行合并报告。参见 合并覆盖率报告- 自定义报告
`js
{
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自定义报告
> 例子: ./test/custom-v8-reporter.js$3
如何配置多个报告
`js
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- 从Node.js
- 同理对于Node.js会保存到全局的
global.__coverage__- 使用CDP
-
getIstanbulCoverage() 参见CDPClient APICollecting V8 Coverage Data
- 在收集V8覆盖率数据之前,需要开启构建工具的sourcemap支持,并且不要压缩代码
- webpack: devtool: source-map and mode: development, example webpack.config-v8.js
- rollup: sourcemap: true and treeshake: false
- esbuild: sourcemap: true, treeShaking: false and minify: false
- vite: sourcemap: true and minify: false- 浏览器 (仅支持基于Chromium的浏览器)
- 使用Playwright
- 使用Puppeteer
- 从Node.js
- 从Node.js收集V8覆盖率数据
- 使用CDP
- 使用
CDPClient API收集V8覆盖率数据$3
使用Playwright的覆盖接口收集覆盖率数据
`js
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, css
$3
使用Puppeteer的覆盖接口收集覆盖率数据,注意Puppeteer默认不会提供原生V8的覆盖率数据,需要设置includeRawScriptCoverage
`js
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];
`
参见: ./test/test-puppeteer.js$3
有多种方法可以从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- 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来开启和收集覆盖率数据
- 参见例子:
> node ./test/test-node-ins.js
- vm的例子 (注意这里需要使用
scriptOffset,因为vm里一般都会加一层包裹代码,需要这个偏移位置来修正覆盖率数据块的位置):
> node ./test/test-node-vm.js
- 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 ./test/test-node-koa.js- Child Process + NODE_V8_COVERAGE
- 如果是子进程,可参见 命令行
$3
- CDPClient为MCR提供的内置接口类,用来更便捷的处理覆盖率相关数据,所有的API如下
`js
// 开始和停止并收集JS的覆盖率数据
startJSCoverage: () => Promise;
stopJSCoverage: () => Promise;// 开始和停止并收集CSS的覆盖率数据,支持匿名文件(比如style里的css)
startCSSCoverage: () => Promise;
stopCSSCoverage: () => Promise;
// 开始和停止并收集JS和CSS的覆盖率数据
startCoverage: () => Promise;
stopCoverage: () => Promise;
/* 如果开启了NODE_V8_COVERAGE,这个接口用来手动保存当前覆盖率数据 /
writeCoverage: () => Promise;
/* 收集istanbul覆盖率数据 /
getIstanbulCoverage: (coverageKey?: string) => Promise;
`- 结合使用Node调试端口
--inspect=9229 或者浏览器调试端口 --remote-debugging-port=9229
`js
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();
`- 结合使用 Playwright CDPSession
`js
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
`js
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浏览器)
`js
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
})
`$3
- JavaScript V8代码覆盖官方说明
- Playwright的覆盖率接口
- Puppeteer的覆盖率接口
- DevTools Protocol的覆盖率接口 参见 ScriptCoverage 和 v8-coverage
`js
// 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
过滤可以使用函数:
`js
const coverageOptions = {
entryFilter: (entry) => entry.url.indexOf("main.js") !== -1,
sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
};
`
也可以使用便捷的minimatch来匹配(推荐):
`js
const coverageOptions = {
entryFilter: "**/main.js",
sourceFilter: "/src/"
};
`
支持多个匹配:
`js
const coverageOptions = {
entryFilter: {
'/node_modules/': false,
'**/vendor.js': false,
'/src/': true
},
sourceFilter: {
'/node_modules/': false,
'/': true
}
};
`
作为CLI参数(JSON字符串,Added in: v2.8):
`sh
mcr --sourceFilter "{'/node_modules/':false,'/':true}"
`
注意,这些匹配实际上会转换成一个过滤函数(如下),所以如果一个匹配成功则会直接返回,后面的将不再继续匹配。请注意先后顺序,如果存在包含关系的,可以调整上下顺序,最后如果都未匹配,则默认返回false
`js
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
}
};
`$3
如果你不想定义两个过滤器,可以使用 filter 选项代替,可以将多个匹配合并在一起. (Added in: v2.8)
`js
const coverageOptions = {
// combined patterns
filter: {
'/node_modules/': false,
'**/vendor.js': false,
'/src/': true
'/': true
}
};
`Resolve
sourcePath for the Source Files
当一个文件从sourcemap解包,它的路径可能是个虚拟路径, 此时可以使用sourcePath选项来修改文件路径。比如,我们测试了多个dist包的入口文件,它们的源文件可能包含了一些共同的文件,但路径可能不同,如果我们需要相同的文件覆盖率数据可以自动合并,那么需要使用sourcePath来统一这些相同文件的路径
`js
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的替换:
`js
const coverageOptions = {
sourcePath: {
'my-dist-file1/': '',
'my-dist-file2/': ''
}
};
`
解决文件路径不完整的问题:
`js
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%
`js
const coverageOptions = {
all: './src', // 支持多个目录
all: ['./src', './lib'],
};
`
未测试的文件也适用于sourceFilter过滤器. 而且也可以指定自己的filter过滤器 (可以返回文件类型来支持js或css的覆盖率格式):
`js
const coverageOptions = {
all: {
dir: ['./src'],
filter: {
// exclude files
'*/ignored-.js': false,
'*/.html': false,
// empty css coverage
'*/.scss': "css",
'*/': true
}
}
};
`
我们可能需要编译.ts, .jsx, .vue等等这样的文件, 这样才能被默认的AST解析器解析,以得到更多的覆盖率指标的数据
`js
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,如果低于要求的值则可以抛出一个错误退出
`js
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 开头可以忽略未覆盖的代码:
- 忽略开始到结束
`js
/ v8 ignore start /
function uncovered() {
}
/ v8 ignore stop /
`
- 忽略接下来一行或者多行
`js
/ 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 的语法格式
`js
/ c8 ignore start /
function uncovered() {
}
/ c8 ignore stop // node:coverage disable /
function uncovered() {
}
/ node:coverage enable /
`Multiprocessing Support
> 多进程支持可以很好的解决异步并行的情况。所有的覆盖率数据会保存到[outputDir]/.cache,在报告生成之后,这些缓存数据会被清除。除非开启了调试模式,或者使用了raw报告
- 主进程,初始化,清理之前的缓存
`js
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
`js
const MCR = require('monocart-coverage-reports');
const coverageOptions = require('path-to/same-options.js');
const mcr = MCR(coverageOptions);
await mcr.add(coverageData1);
`- 子进程2, 测试业务2
`js
const MCR = require('monocart-coverage-reports');
const coverageOptions = require('path-to/same-options.js');
const mcr = MCR(coverageOptions);
await mcr.add(coverageData2);
`- 主进程,所有测试完成之后
`js
// 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目录来读取覆盖率数据,并生成覆盖率报告- 全局安装
`sh
npm i monocart-coverage-reports -g
mcr node ./test/specs/node.test.js -r v8,console-details --lcov
`- 本地项目安装
`sh
npm i monocart-coverage-reports
npx mcr node ./test/specs/node.test.js -r v8,console-details --lcov
`- 命令行参数
直接运行
mcr 或 mcr --help 查看所有CLI的参数- 使用
-- 可以隔离子程序参数,以免两种参数混淆
`sh
mcr -c mcr.config.js -- sub-cli -c sub-cli.config.js
`- 参见例子
- Mocha
- TypeScript
- AVA
Config File
根据以下优先级加载配置文件
- 自定义配置文件(如果没有指定则加载后面的默认配置文件):
- CLI: mcr --config
- API: await mcr.loadConfig("my-config-file-path")
- mcr.config.js
- mcr.config.cjs
- mcr.config.mjs
- mcr.config.json - json format
- mcr.config.ts (requires preloading the ts execution module)Merge Coverage Reports
以下这些使用场景可能需要使用合并覆盖率报告:
- 多个执行环境,比如Node.js服务端,以及浏览器客户端,比如Next.js
- 多种测试类型,比如Jest单元测试,以及Playwright的端到端自动化测试
- 分布式测试,测试结果保存到了多台机器或不同的容器中$3
- 默认MCR在执行generate()时会自动合并覆盖率数据。所以可以在多进程支持下,多次添加覆盖率数据,最后将自动合并
- 比如Next.js就可以同时添加前后端覆盖率数据,最后再执行generate()生成覆盖率报告,见例子nextjs-with-playwright
- 使用Codecov在线覆盖率报告服务,请设置输出codecov报告, 它会生成专属的codecov.json,如果有多个codecov.json文件上传,它们会自动合并数据,参见Codecov 和 合并报告说明$3
手动合并覆盖率报告需要使用raw报告来导出原始的覆盖率数据到指定的目录
- 比如,单元测试保存到./coverage-reports/unit/raw,见例子
- Jest + jest-monocart-coverage
- Vitest + vitest-monocart-coverage
`js
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 API
- Playwright + MCR, see playwright-coverage
- see more Integration Examples- 然后创建一个
merge-coverage.js文件,使用inputDir参数导入raw数据,来生成合并的覆盖率报告.
`js
// 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. 所有的执行脚本大概如下:
`json
{
"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-coverageCommon issues
> 常见问题
$3
覆盖率看起来不正确,多数情况是因为sourcemap转换的问题导致的. 可以先尝试设置构建工具的 minify=false 也就是不要压缩代码来解决。下面来看看sourcemap存在问题的具体原因:
`js
const a = tf ? 'true' : 'false';
^ ^ ^
m1 p m2
`
上面是经过构建工具编译过的代码,通过AST分析,位置p对应的原始位置是我们要找的,而从sourcemap里仅能找到离p最近的位置映射m1和m2,也就是位置p并没有精确的映射保存到sourcemap里,从而无法直接获取精确的原始位置,但我们能知道p的原始位置应该在m1和m2之间。
- 参见 调试覆盖率和sourcemapMCR如何解决这个问题:
- 1, 首先会尝试使用diff-sequences工具来比较m1和m2之间的生成代码和原始代码,找到p对应的字符位置,可以解决绝大多数问题。但是如果代码是非JS格式的,比如Vue模板是HTML,或JSX这些,不管怎么比较也是很难精确找到对应位置的,甚至此时的sourcemap本身都比较乱。
- 2, 然后就是通过分析AST,找到所有的functions, statements 和 branches,因为V8覆盖率本身不提供这些指标的覆盖率. (对于分支覆盖暂不支持AssignmentPattern类型,因为即使分析AST也无法从V8覆盖率找到它的数据)。
$3
源码无法解析问题。由上面我们知道MCR通过分析源码的AST获取更多指标的覆盖率信息,但源码如果不是标准的 ECMAScript,比如ts, jsx这些,那么分析的时候就会报错,此时我们可以手动来编译这些文件(可行但不推荐).
`js
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;
}
}
`$3
内存溢出问题可能出现在有太多的原生V8覆盖率文件要处理. 我们可以使用Node.js的一个选项来增加内存使用:
`sh
- run: npm run test:coverage
env:
NODE_OPTIONS: --max-old-space-size=8192
`
Debug for Coverage and Sourcemap
> 当你觉得覆盖率存在问题的时候,MCR支持自行调试来核验覆盖率的准确性
- 首先打开调试设置logging: 'debug'
`js
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文件夹
`js
const coverageOptions = {
logging: 'debug',
sourceMap: true
};
`- 使用环境变量
MCR_LOG_TIME显示时间日志
`js
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
- 参见 命令行Integration Examples
$3
- 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:
- playwright-ct-vue
- playwright-ct-react
- playwright-ct-svelte
- Coverage for Next.js, both server side and client side:
- nextjs-with-playwright
- nextjs-with-playwright-istanbul
- Coverage for Remix:
- remix-with-playwright
- see Collecting V8 Coverage Data with Playwright$3
- c8 has integrated MCR as an experimental feature since v10.1.0
`sh
c8 --experimental-monocart --reporter=v8 --reporter=console-details node foo.js
`$3
- CodeceptJS is a BDD + AI testing framework for e2e testing, it has integrated MCR since v3.5.15, see plugins/coverage$3
- Monocart Coverage for VSCode - Shows native V8 code coverage in VSCode$3
- jest-monocart-coverage - Jest custom reporter for coverage reports
- merge-code-coverage - Example for merging code coverage (Jest unit + Playwright e2e sharding)$3
- vitest-monocart-coverage - Vitest custom provider module for coverage reports
- merge-code-coverage-vitest - Example for merging code coverage (Vitest unit + Playwright e2e sharding)$3
- node-monocart-coverage - Custom reporter for Node test runner for coverage$3
- 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$3
- cypress-monocart-coverage - Cypress plugin for coverage reports$3
- wdio-monocart-service - WebdriverIO service for coverage reports$3
- storybook-monocart-coverage - Example for Storybook V8 coverage reports$3
- testcafe-reporter-coverage - TestCafe custom reporter for coverage reports$3
- selenium-webdriver-coverage - Example for Selenium Webdriver V8 coverage reports$3
`sh
mcr mocha ./test/*/.js
`$3
- tsx
`sh
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
`
- ts-node
`sh
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
`
$3
`sh
mcr ava
`$3

- Supports native codecov built-in report (specification)
`js
const coverageOptions = {
outputDir: "./coverage-reports",
reports: [
['codecov']
]
};
`
- Github actions:
`yml
- name: Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage-reports/codecov.json
`$3

- Using lcov report:
`js
const coverageOptions = {
outputDir: "./coverage-reports",
lcov: true
};
`
- Github actions:
`yml
- name: Codacy Coverage Reporter
uses: codacy/codacy-coverage-reporter-action@v1
with:
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
coverage-reports: ./docs/mcr/lcov.info
`$3

- Using lcov report:
`js
const coverageOptions = {
outputDir: "./coverage-reports",
lcov: true
};
`
- Github actions:
`yml
- name: Coveralls
uses: coverallsapp/github-action@v2
with:
files: ./coverage-reports/lcov.info
`$3

- Using lcov report. Github actions example:
`yml
- 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)
`sh
npm install
npx playwright install --with-depsnpm run build
npm run test
npm run dev
`
- Refreshing eol=lf for snapshot of test (Windows)
`sh
git add . -u
git commit -m "Saving files before refreshing line endings"npm run eol
``