测试质量度量

评估一个测试套件的“含金量”,我们通常关注如下四个维度:

本节将讨论前三个维度,而将最后一个维度留在下一节讨论。

代码覆盖率

覆盖率及度量标准

代码覆盖率衡量的是在运行测试套件时,生产代码中有多少行/分支被执行到了。

其度量标准有:

  • 行覆盖率 (Line Coverage): 执行到的代码行数百分比。
  • 分支覆盖率 (Branch Coverage): if/elseswitch 等逻辑分支的覆盖情况。

注:在目前的 Dart SDK 版本(3.8)中,原生覆盖率工具并不支持 Branch Coverage。

实战1:覆盖率

在 Dart 的原生测试流程中,dart test --coverage=<coverage_data_dir> 默认生成的是 .vm.json 格式的原始数据。要将这些 JSON 文件转换为人类可读的 HTML 报告,需要先将其转换为 LCOV 格式,再生成 HTML。 整个过程可以参考如下脚本:

#!/bin/bash
# gencov.sh

# 检查 coverage 工具是否已全局安装
if ! command -v format_coverage &> /dev/null; then
    echo "正在安装 coverage 工具..."
    dart pub global activate coverage
fi

# 移除旧的覆盖率数据
rm -rf coverage/

# 运行测试并生成 JSON 数据
dart test --coverage=coverage

# 格式化转换 
dart pub global run coverage:format_coverage \
  --in=coverage \
  --out=coverage/lcov.info \
  --report-on=lib \
  --lcov \
  --check-ignore

# 生成可视化报告
if [ -f "coverage/lcov.info" ]; then
    genhtml coverage/lcov.info -o coverage/html
    echo "✅ 报告生成成功: coverage/html/index.html"
    # open coverage/html/index.html
else
    echo "❌ 失败:未找到 lcov.info"
fi

理想的测试覆盖率应遵循金字塔结构。

测试类型参考比例度量重点
Integration/E2E Tests (集成测试)5-10%跨层级流转、真实数据库/网络请求。速度慢,维护成本高。
Widget/Component Tests (组件测试)15-20%UI 交互、组件渲染逻辑。
Unit Tests (单元测试)70-80%核心逻辑、算法、纯 Dart 代码。速度极快。

具体的测试覆盖率分配参考:

  • 核心业务逻辑 (Services/Models):目标 95%+。这里是 Bug 出现最多的地方。重点测试各种边界条件、异常处理、状态转换。
  • 工具类 (Utils):目标 100%。工具函数必须绝对纯净可靠。
  • UI 渲染层 (Widgets):目标 40% - 60%。重点测试交互逻辑和状态流转而非视觉表现。
  • 第三方配置 (Config/Firebase):目标 0%。排除这些文件,避免虚高的覆盖率掩盖真实问题。

测试有效性

如果说覆盖率是评估测试的广度,那么测试有效性(Test Effectiveness)则是评估测试的深度与真实性。测试有效性关注的是:如果代码出现了逻辑错误,现有的测试套件是否会报错(Fail)?

提升测试有效性的三个维度

拒绝“为了覆盖而覆盖”的测试

有些开发者为了刷指标,会写出没有 expect() 的测试。

test('void test', () {
  someFunction(); // 跑过了,覆盖率涨了,但没有任何断言
});

此类无效测试增加了维护成本,提供的却是虚假的安全感。

边界值与等价类划分

有效的测试不应只测“正常情况”,还要重点关注边界值与异常值。

  • 正常值:2 / 1 = 2
  • 边界值:最大整数、空字符串、数值区间的边界等。
  • 异常值:传入非预期的 Null、0、负数或格式错误的 JSON 等。

模拟真实环境

如果 Mock 对象总是返回正确的结果,那么永远测不到代码处理网络超时500 错误的逻辑。因此我们必须有意识地编写针对各类异常情况的负向测试用例(Negative Cases)。

变异测试

衡量测试有效性最科学的方法是变异测试 (Mutation Testing),其原理是:工具会自动修改你的源代码(称为“注入变异”),例如将 a > b 改为 a >= b,或者将 + 改为 -

指标

  • 被杀死的变异体 (Killed Mutants):测试失败了。说明你的测试有效,捕捉到了逻辑变化。
  • 存活的变异体 (Survived Mutants):测试依然通过。说明你的测试存在盲区,即使逻辑改错了也发现不了。
  • 变异分数 (Mutation Score)

$$Mutation\ Score = \frac{\text{Killed Mutants}}{\text{Total Mutants}} \times \text{100%}$$

变异分数解读

  • 80%+:极高。代码逻辑极其稳健。
  • 60% - 80%:良好。核心逻辑有保障。
  • 40%:危险。即便覆盖率是 100%,测试也可能只是在“空跑”。

实战2: 变异测试

下面我们拿之前编写的 fraction.dart 演示一下变异测试。

首先安装工具(mutation_test):

dart pub global activate mutation_test

然后编写一个规则文件 (mutation-test.yaml,此例只测试lib/fraction.dart):

<?xml version="1.0" encoding="UTF-8"?>
<mutations version="1.1">
  <commands>
    <command group="test" timeout="10">dart test test/fraction_test.dart</command>
  </commands>

  <exclude>
    <file>lib/hellodart.dart</file>
    
    <regex pattern=".*\.g\.dart" />
    <directory>test</directory>

    <token begin="//" end="\n" />
    <regex pattern="/\*[\s\S]*?\*/" dotAll="true" />
    <token begin="///" end="\n" />

    <regex pattern="String toString\(\)[\s\S]*?\}" />
  </exclude>

  <rules>
    <literal text="+"> <mutation text="-"/> </literal>
    <literal text="-"> <mutation text="+"/> </literal>
    <literal text="*"> <mutation text="/"/> </literal>
    <literal text="/"> <mutation text="*"/> </literal>

    <literal text="=="> <mutation text="!="/> </literal>
    <literal text="&lt;0"> <mutation text="&gt;0"/> </literal> 
    <literal text="~/ "> <mutation text="/ "/> </literal>

    <regex pattern="b == 0 \? a :">
      <mutation text="b != 0 ? a :"/>
      <mutation text="b == 0 ? b :"/>
    </regex>
  </rules>

  <threshold failure="80">
    <rating over="80" name="A - 极高可靠性" />
    <rating over="60" name="B - 逻辑达标" />
  </threshold>
</mutations>

最后运行变异测试:

$ dart pub global run mutation_test --rules mutation-rules.xml
Found 30 mutations in 1 source files!
lib/fraction.dart : 30 mutations
OK: 3/30 (10.00%) mutations were not detected!                                 
  --- Results ---
Test group statistics:
  Group : test, Found mutations: 27

Total tests: 30
Undetected Mutations: 3 (10.00%)
Timeouts: 0
Not covered by tests: 0
Elapsed: 0:01:15.163499
Success: true, Quality rating: A - 极高可靠性
Output has been written to mutation-test-report

测试报告输出至 mutation-test-report 文件夹中,可通过浏览器查看(mutation-test-report/mutation-test-report.html)。

测试运行性能

如果测试运行太慢,CI/CD (持续集成/开发)流水线会变成开发的瓶颈。在 Dart 和 Flutter 生态中,优化测试性能有其独特的策略。

并行测试

Dart 的测试运行器默认会尝试并行运行不同的测试文件。因此应该将一个大的测试文件拆分成多个小文件。实战中我们指定测试并行数(根据 CPU 核心数调整):

dart test -j 4

延迟初始化

setUpAll 用于初始化在某个测试组中通用的一些对象,例如 Mock 数据库。此类操作往往昂贵(费时),可采用延迟初始化的策略 —— 只在真正需要时才执行。

优化变异测试的性能

增量变异 (Incremental Analysis)

在 CI 中,可以通过 git diff 只针对本次修改的文件运行变异测试。

dart pub global run mutation_test \
  --rules mutation-rules.xml \
  $(echo $(git diff --name-only HEAD HEAD~1 | grep -v "^test" | grep ".dart$" | tr '\n' ' '))

利用 LCOV 数据

mutation_test 支持读取覆盖率文件。如果某行代码根本没被覆盖,工具会直接跳过变异,从而节省大量无效的测试运行。

dart test --coverage=coverage

dart pub global run mutation_test \
  --rules mutation-rules.xml \
  --coverage coverage/lcov.info

Reference