跳转到正文
YGJoon 技术博客

Flutter 测试覆盖率详解

发布于:
·1.3k 字 · 约 3 分钟·作者:
flutter test --coverage 收集 lcov 数据,genhtml 生成可视化报告,行覆盖与分支覆盖两个维度看测试盲区
Flutter 测试覆盖率详解文章封面
目录

简介

测试覆盖率是衡量测试完整性的重要指标。本文介绍如何在 Flutter 中统计和分析测试覆盖率。

基本配置

添加依赖

dev_dependencies:
  flutter_test:
    sdk: flutter
  coverage: ^1.6.3

运行命令

## 运行测试并收集覆盖率
flutter test --coverage

## 生成 HTML 报告
genhtml coverage/lcov.info -o coverage/html

覆盖率类型

1. 行覆盖率

// 统计每行代码是否被执行
@override
Widget build(BuildContext context) {
  if (condition) {  // 已覆盖
    return Widget1();  // 已覆盖
  } else {
    return Widget2();  // 未覆盖
  }
}

2. 分支覆盖率

String getMessage(int value) {
  if (value > 0) {  // true 分支已覆盖
    return 'Positive';
  } else if (value < 0) {  // false 分支未覆盖
    return 'Negative';
  } else {  // 未覆盖
    return 'Zero';
  }
}

完整示例

// 被测试的类
class Calculator {
  int add(int a, int b) => a + b;
  int subtract(int a, int b) => a - b;
  int multiply(int a, int b) => a * b;
  
  double divide(int a, int b) {
    if (b == 0) {
      throw ArgumentError('Cannot divide by zero');
    }
    return a / b;
  }
  
  bool isEven(int number) => number % 2 == 0;
  
  String getNumberType(int number) {
    if (number > 0) {
      return 'Positive';
    } else if (number < 0) {
      return 'Negative';
    } else {
      return 'Zero';
    }
  }
}

// 测试文件
void main() {
  group('Calculator', () {
    late Calculator calculator;
    
    setUp(() {
      calculator = Calculator();
    });
    
    group('add', () {
      test('should add two positive numbers', () {
        expect(calculator.add(2, 3), equals(5));
      });
      
      test('should add positive and negative numbers', () {
        expect(calculator.add(2, -3), equals(-1));
      });
    });
    
    group('divide', () {
      test('should divide two numbers', () {
        expect(calculator.divide(6, 2), equals(3));
      });
      
      test('should throw error when dividing by zero', () {
        expect(
          () => calculator.divide(6, 0),
          throwsA(isA<ArgumentError>()),
        );
      });
    });
    
    group('getNumberType', () {
      test('should return Positive for positive numbers', () {
        expect(calculator.getNumberType(5), equals('Positive'));
      });
      
      test('should return Negative for negative numbers', () {
        expect(calculator.getNumberType(-5), equals('Negative'));
      });
      
      test('should return Zero for zero', () {
        expect(calculator.getNumberType(0), equals('Zero'));
      });
    });
  });
}

覆盖率报告

1. 命令行报告

## 显示覆盖率摘要
flutter test --coverage --coverage-path=coverage/lcov.info

## 生成详细报告
genhtml coverage/lcov.info -o coverage/html

2. HTML 报告

<!DOCTYPE html>
<html>
<head>
  <title>Coverage Report</title>
</head>
<body>
  <div class="coverage-report">
    <h1>Coverage Report</h1>
    <div class="summary">
      <p>Line Coverage: 85%</p>
      <p>Branch Coverage: 75%</p>
      <p>Function Coverage: 90%</p>
    </div>
    <!-- 详细报告内容 -->
  </div>
</body>
</html>

CI/CD 集成

GitHub Actions

name: Test Coverage

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.x'
    
    - name: Install dependencies
      run: flutter pub get
    
    - name: Run tests with coverage
      run: flutter test --coverage
    
    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        file: coverage/lcov.info

最佳实践

  1. 设置覆盖率目标
// 在 CI 中设置最低覆盖率要求
final minimumCoverage = 80.0;
final coverage = calculateCoverage();
if (coverage < minimumCoverage) {
  throw Exception('Coverage below minimum threshold');
}
  1. 忽略不需要测试的代码
// coverage:ignore-start
void debugLog(String message) {
  print(message);
}
// coverage:ignore-end
  1. 定期检查覆盖率趋势
class CoverageTrend {
  final DateTime date;
  final double coverage;
  
  CoverageTrend(this.date, this.coverage);
  
  static void trackCoverage(double coverage) {
    // 记录覆盖率历史
  }
}

注意事项

  1. 别为数字而测试:expect(true, true) 式的断言能把覆盖率刷到 100%,但 bug 照样漏(实战案例)。review 测试时先看断言是不是在断言业务,再看覆盖率数字。
  2. 先覆盖关键路径:登录、支付、工单列表渲染这种核心流程(实战案例的策略),再谈整体数字。一个 60% 但核心全覆盖的项目,比 85% 但支付没测的项目更安全。
  3. 覆盖率门禁设在增量上:全量 80% 的硬门槛会逼大家写水测试。更有效的是”新增代码覆盖率不低于 70%” + 核心目录单独设线——存量代码慢慢还。
  4. coverage:ignore 要有理由:生成的代码(.g.dart、mockito 生成的 mock)本来就该忽略(正文最佳实践第 2 条已写);手写业务代码加 ignore 必须在 PR 里说明原因,不然就是给自己开后门。
  5. 分支覆盖比行覆盖诚实:getNumberType 三个 return 全执行了,行覆盖 100%,但只测了正数就漏了负数和零分支。看报告时先看分支覆盖(正文”覆盖率类型”一节)。

实战案例

覆盖率这个指标,我的项目里是零——电商奢侈品专区改版有 widget 测试,但从来没跑过 flutter test --coverage,更没设过门禁。诚实说,小团队赶业务时,覆盖率数字是最容易自欺欺人的:100% 覆盖的屎山我也见过(全是 expect(true, true) 式测试)。我的实际策略是”关键路径覆盖”:工单列表这种核心页面的渲染测试必须有(奢侈品专区那组),工具类(如 JsonConverter)值得补单测,剩下的随团队风险取舍。覆盖率是手段不是目标,与其追求 80% 的数字,不如保证核心流程的测试真的在断言业务。等团队需要质量门禁时,再按关键路径补齐。

总结

测试覆盖率是衡量测试质量的重要指标,但不是唯一指标。通过合理的覆盖率统计和分析,可以帮助我们发现测试盲点,提高代码质量。


分享这篇文章:


相关文章

上一篇
Flutter Scaffold 组件详解
下一篇
Flutter 实现应用内更新