DevEco Studio支持黑盒覆盖率测试,不需要开发测试用例,将编译插桩的HAP包推到设备上,然后对该应用/元服务模拟用户操作,测试完成后可生成覆盖率报告,当前仅支持Stage模型。
从DevEco Studio 5.1.0 Release版本开始,支持自定义参与覆盖率测试的文件。在工程目录下创建coverage-filter.json5文件,在文件中配置相关字段,DevEco Studio编译插桩时将按照coverage-filter.json5文件中的配置进行过滤。

修改配置文件后不会触发增量编译,需要重新编译插桩再测试。
coverage-filter.json5文件支持以下字段。
字段名称 | 可选/必选 | 类型 | 适用的覆盖率测试 | 含义 |
|---|---|---|---|---|
include | 可选 | 字符串数组 | 黑盒覆盖率测试 仪器覆盖率测试 本地覆盖率测试 | 配置参与覆盖率测试的文件或文件夹路径。
说明: include的优先级比exclude高。 |
exclude | 可选 | 字符串数组 | 黑盒覆盖率测试 仪器覆盖率测试 本地覆盖率测试 | 配置不参与覆盖率测试的文件或文件夹路径。
|
includeHar | 可选 | 字符串数组 | 黑盒覆盖率测试 仪器覆盖率测试 本地覆盖率测试 | 配置参与覆盖率测试的远程源码har包,不支持远程字节码har包,不支持通配符。 从26.0.0版本开始支持。 |
extraAbilities | 可选 | 对象数组 | 黑盒覆盖率测试 | 如果module.json5中配置的ability是通过import外部的ability类实现,同时在当前文件通过export导出,此ability无法生成黑盒覆盖率数据。可配置extraAbilities>path字段,填写外部的ability路径,生成黑盒覆盖率数据。支持相对路径,不支持通配符。示例如下。 从26.0.0版本开始支持。 |
extraAbilities字段示例:
// xxx.ets
import EntryAbility from './EntryAbility';
export default EntryAbility;
// coverage-filter.json5
{
"extraAbilities": [
{
"path": "entry/src/main/ets/entryability/EntryAbility.ets", // import的EntryAbility的相对路径
},
],
} // 26.0.0及以上版本
{
"include":[
"entry/src/main/ets/pages" // 无通配符,包括pages下所有文件
],
"exclude":[
"**/src/**/HspTest?.ets", // src目录下的HspTest1.ets、HspTest2.ets等文件
"**/src/**/*Test1.*", // src目录下所有文件名中有Test1.的文件
"**/src/**/utils" // src目录底下所有utils文件夹里的文件
],
"includeHar":[
"math"
],
"extraAbilities": [
{
"path": "entry/src/main/ets/entryability/EntryAbility.ets"
}
]
}
// 26.0.0以下版本
{
"include":[
"entry/src/main/ets/pages/aaa.ets"
],
"exclude":[
"entry/src/main/ets/pages"
]
} 有两种方式进行编译与安装,DevEco Studio方式和命令行方式,具体步骤如下。

,DevEco Studio会启动编译插桩,并推包安装到设备上。hvigorw --mode module -p module={moduleName@targetName} -p product={productName} -p buildMode=test -p ohos-test-coverage=true -p coverage-mode=black assembleHap --parallel --incremental --daemon
图标进行查看。
如果HAP依赖HSP,需要单独编译HSP,将以上命令的assembleHap替换为assembleHsp即可。
hdc uninstall {bundleName} hdc install {SignedHapPath} hdc shell aa dump -c -l
const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
context.eventHub.emit('coverage');
从API 13开始,如果用户使用最近任务列表一键清理来关闭应用,将不会执行onDestroy()回调,导致获取不到覆盖率数据。
// 如果是应用则执行该命令,其中LocalPath非必填,如果不填写,默认存放在当前执行命令的目录
hdc file recv data/app/el2/100/base/{bundleName}/haps/{moduleName}/cache {LocalPath}
// 如果是元服务则执行该命令,其中LocalPath必填
hdc file recv -b {bundleName} ls ./data/storage/el2/base/haps/{moduleName}/cache {LocalPath}
在多模块相互跳转的场景下,只需要取最后退出的模块下生成的覆盖率数据json文件,但特殊场景下如多模块无跳转关系,则需要取每个独立模块下生成的覆盖率数据json文件。
hvigorw collectCoverage -p projectPath={projectPath} -p reportPath={reportPath} -p coverageFile={projectPath}/{moduleName}/.test/default/intermediates/ohosTest/init_coverage.json#{LocalPath/bjc_cov_yyyyMMdd_HHmmss_SSS.json} -p patch={patchPath}
在多模块相互跳转的场景下,需要取各模块的init_coverage.json文件路径,与bjc_cov_yyyyMMdd_HHmmss_SSS.json文件通过#拼接生成coverageFile参数。

执行覆盖率测试后,会生成两份报告,一份是html格式,用于可视化查看报告,一份是json格式,即coverageReport.json文件,记录了详细的覆盖率数据,文件中各字段的含义请参考覆盖率coverageReport.json文件。
测试覆盖率报告有三个测量维度,分别是:

以下是关于三个测量维度的细节说明:
常见的流程控制语句有if、while、do...while、switch、for等等,以及三目运算符(condition ? exprIfTrue : exprIfFalse),需要确保流程控制的每个边界情况(即分支)都被执行。
箭头函数在方法内时,可以正常统计覆盖率,如果作为参数声明,则无法统计该行覆盖率。
示例如下:
import { window } from '@kit.ArkUI'; // +0 方法外不统计
let filePath :string; // +0 方法外不统计
const fileName = 'a.txt'; // +0 方法外不统计
export function doTheThing () // +1
{ // +1
let s1: string; // +1
const str = 'aaa'; // +1
console.log(str); // +1
} // +0
export class Person { // +0 方法外不统计
name: string = '' // +0 方法外不统计
constructor (n:string) { // +1 构造函数
this.name = n; // +1
} // +0
static sayHello () { // +1 类静态方法
console.log('hello'); // +1
} // +0
walk () { // +1 类实例方法
for ( // +1
let i=0; // +1
i < 10; // +1
i++) // +1
{ // +1
} // +0
} // +0
} // +0
function func ():object { // +1
return Object({ // +1 一个语句被拆分为多行
a: 1, // +1
b: 2, // +1
}) // +0
} // +0
func(); // +0 方法外不统计
function foo(n:number, m:number){} // +1
function bar():number { // +1
return 1; // +1
}
foo(1, bar()); // +0 方法外不统计 
代码中的某些分支可能很难、甚至无法测试,DevEco Studio提供了instrument ignore * 语法来进行忽略,使得某些代码不计入覆盖率。
使用时需先清除缓存,点击菜单栏Build -> Clean Project。
import {testA} from './Index'
// instrument ignore file 忽略整个文件
// instrument ignore next 忽略代码块
export function sum(a:number,b:number){
return a+b;
}
sum(1,2);
let a = 1;
// instrument ignore else 忽略else分支
if (a!=1) {
// do something
console.log('BBB');
}else {
console.log('AAA');
}
// instrument ignore if 忽略if分支
if (a==1) {
// do something
console.log('BBB');
}else {
console.log('AAA');
} 覆盖率coverageReport.json文件记录了详细的覆盖率数据,文件中各字段的含义如下。
在阅读本文前,请先查看覆盖率报告解读,了解行覆盖率、分支覆盖率和函数覆盖率的相关概念和统计方式。
{
"summary": {
"lines": { // 行数总览
"total": 43, // 可执行行代码行数
"covered": 12, // 覆盖数量
"pct": 27.91 // 行覆盖率
},
"functions": { // 函数总览
"total": 17, // 函数数量
"covered": 4, // 覆盖数量
"pct": 23.53 // 函数覆盖率
},
"branches": { // 分支总览
"total": 2, // 分支数量
"covered": 0, // 覆盖数量
"pct": 0 // 分支覆盖率
}
},
} {
"files": [
{
"version": "bjc v1.0.0", // 覆盖率算法版本
"versionCode": 10000, // 覆盖率算法版本代码
"path": "D:/DevEcoStudioProjects/MyApplication36/application/src/main/ets/applicationability/ApplicationAbility.ets", // 文件路径
"hash": "6828362e96a78934b93db4b980fa5ad83af85a111bf187e74da89ae0c0ec613a", // 文件内容hash值
"lineCnt": 44, // 当前文件总行数
"count": 0, // 执行次数
"projectPath": "D:/DevEcoStudioProjects/MyApplication36", // 工程路径
"functions": [], // 函数集合
"exeLine": {}, // 可执行代码行
"summary": {} // 单个文件的覆盖率详情
},
]
} functions是个数组,记录了文件中所有函数的详细覆盖率数据,数组中的每个元素对应一个函数。
"functions": [
{
"name": "ApplicationAbility.onCreate", // 函数名称,如果是匿名函数,name是anonymous_N
"count": 0, // 函数执行次数
"regions": [], // 对应代码区域
"branches": [], // 分支
"ignored": 0, // 函数忽略次数
"index": 0 // 函数在整个文件中的位置,从0开始排序
},
] regions是一个可执行行数组,数组可能有一个元素、两个元素或多个元素。
第一个元素是该方法对应的代码区域,如果不止一个元素,后面的元素是方法内的可执行代码区域。元素中每个字段的含义如下。
"regions": [
{
"startLoc": { // 开始代码位置
"line": 8, // 起始行号
"col": 3 // 起始列号
},
"endLoc": { // 结束代码位置
"line": 10, // 结束行号
"col": 4 // 结束列号
},
"count": 0, // 执行次数
"ignored": 0 // 忽略次数
}
] 
{
"name": "dddd",
"count": 1,
"regions": [
{
"startLoc": {
"line": 31,
"col": 1
},
"endLoc": {
"line": 33,
"col": 2
},
"count": 1,
"ignored": 0
}
],
}, 
{
"name": "aaaaa",
"count": 2,
"regions": [
{ // 方法对应的代码区域
"startLoc": {
"line": 2,
"col": 1
},
"endLoc": {
"line": 9,
"col": 2
},
"count": 2,
"ignored": 0
},
{ // 可执行代码区域
"startLoc": {
"line": 4,
"col": 3
},
"endLoc": {
"line": 9,
"col": 2
},
"count": 2,
"ignored": 0
}
],
} 
{
"name": "bbb",
"count": 1,
"regions": [
{ // 方法对应的代码区域11-18行
"startLoc": {
"line": 11,
"col": 1
},
"endLoc": {
"line": 18,
"col": 2
},
"count": 1,
"ignored": 0
},
{ // 第一个可执行代码区域13-15行
"startLoc": {
"line": 13,
"col": 13
},
"endLoc": {
"line": 15,
"col": 4
},
"count": 0, // 由于flag是false,代码未执行
"ignored": 0
},
{ // 第二个可执行代码区域15-17行
"startLoc": {
"line": 15,
"col": 10
},
"endLoc": {
"line": 17,
"col": 4
},
"count": 1, // 代码被执行
"ignored": 0
}
],
} branches是个分支数组,会将if和switch case这种条件判断语句相关的代码块放入数组中,数组中每个元素的字段含义如下。
"branches": [
{
"startLoc": { // 开始代码位置
"line": 46, // 起始行号
"col": 10 // 起始列号
},
"endLoc": { // 结束代码位置
"line": 46, // 结束行号
"col": 11 // 结束列号
},
"trueCount": 0, // 该行满足条件的已执行次数,0表示未执行
"falseCount": 1, // 该行不满足条件的已执行次数,0表示未执行
"group": [ // 分组,if语句不涉及分组,switch case涉及分组
0, // group:[0,1],表示branches数组的0号和1号元素属于一个switch case
1
],
"ignored": 0 // 忽略次数
}
] 示例一:调用eeee(2)。

{
"name": "eeee",
"count": 1,
"regions": [
// ...
],
"branches": [
{
"startLoc": {
"line": 46,
"col": 10
},
"endLoc": {
"line": 46,
"col": 11
},
"trueCount": 0, // 该行条件未执行
"falseCount": 1, // 该行已执行,但不满足条件
"group": [ // 0和1号元素属于1个switch case,2和3号元素属于另一个switch case
0,
1
],
"ignored": 0
},
{
"startLoc": {
"line": 49,
"col": 10
},
"endLoc": {
"line": 49,
"col": 11
},
"trueCount": 1,
"falseCount": 0,
"group": [
0,
1
],
"ignored": 0
},
{
"startLoc": {
"line": 55,
"col": 10
},
"endLoc": {
"line": 55,
"col": 13
},
"trueCount": 0,
"falseCount": 1,
"group": [
2,
3
],
"ignored": 0
},
{
"startLoc": {
"line": 58,
"col": 10
},
"endLoc": {
"line": 58,
"col": 13
},
"trueCount": 1,
"falseCount": 0,
"group": [
2,
3
],
"ignored": 0
}
],
} 示例二:调用bbb(2)和bbb(-1),该方法触发两次。

branches的0号元素,对应12行,trueCount和falseCount都为1,表示该行触发了两次,一次满足条件,一次不满条件。
{
"name": "bbb",
"count": 2,
"regions": [
// ...
],
"branches": [
{
"startLoc": {
"line": 12,
"col": 7
},
"endLoc": {
"line": 12,
"col": 12
},
"trueCount": 1,
"falseCount": 1,
"group": [],
"ignored": 0
},
{
"startLoc": {
"line": 14,
"col": 14
},
"endLoc": {
"line": 14,
"col": 20
},
"trueCount": 0,
"falseCount": 1,
"group": [],
"ignored": 0
},
{
"startLoc": {
"line": 22,
"col": 7
},
"endLoc": {
"line": 22,
"col": 13
},
"trueCount": 1,
"falseCount": 1,
"group": [],
"ignored": 0
}
]
} exeLine记录了所有可执行行的行号,示例如下。

生成的exeLine为:
"exeLine": {
"0": 2,
"1": 3,
"2": 4,
"3": 6,
"4": 7,
"5": 8,
} summary记录了单个文件的覆盖率详情。
"summary": {
"lines": { // 行数总览
"total": 10, // 可执行代码行数
"covered": 5, // 覆盖数量
"pct": 50, // 行覆盖率
"executedLineCount": [ // 代码行执行次数,-1表示该行不被统计,0表示未执行,1-N表示执行1-N次
-1,
-1,
-1,
0,
-1,
1,
0,
2,
2,
-1,
1,
1,
0,
0,
0,
-1
]
},
"functions": { // 函数总览
"total": 6, // 函数数量
"covered": 5, // 覆盖数量
"pct": 83.33 // 函数覆盖率
},
"branches": { // 分支总览
"total": 2, // 分支数量
"covered": 1, // 覆盖数量
"pct": 50 // 分支覆盖率
}
}