第一个自动化测试用例

编写代码只是开发的一半,另一半则是确保这些代码在复杂的生产环境中能够持续稳定地运行。本节先简要介绍一下自动化测试,然后聚焦到单元测试,开始我们的Dart测试之旅。

自动化测试

在前面的章节中,我们编写了很多示例。让我们思考这样一问题:我如何确保代码在重构或升级后依然能正常工作?答案就是自动化测试。它不仅是质量的底线,更是开发者重构代码时的“安全网”。

自动化测试包含单元测试、组件测试、集成测试。在现代软件工程中,我们将这三者构成的体系称为测试金字塔(Test Pyramid)

  • 单元测试 (Unit Testing):测试最小的功能单元(如函数或类)。它是金字塔的基石,运行最快且成本最低。
  • 组件测试 (Component Testing):验证多个单元或 UI 组件之间的协作。
  • 集成测试 (Integration Testing):从用户视角出发,验证整个应用在真实环境下的端到端流程。
graph TD
    Top@{ shape: tri, label: "<b>集成测试</b> <br/> 链路验证" }
    Middle[/.....<b>组件测试</b>..... <br/> 模块协作\]
    Bottom[/...........<b>单元测试</b>........... <br/> 最小逻辑单元 \]

    Top --- Middle --- Bottom

    style Top fill:#f8d7da,stroke:#dc3545
    style Middle fill:#fff3cd,stroke:#ffc107
    style Bottom fill:#d4edda,stroke:#28a745

第一个单元测试用例

现代化编程语言的竞争,早已从语法层面延伸到了 工具链(Toolchain) 的完备性。而在工具链中,自动化测试框架无疑是保障工程质量的核心基石。Go 语言秉持“开箱即用”的理念,原生内置了极其强大的测试框架,不仅支持基础的单元测试,还原生支持性能基准测试(Benchmark)以及极具特色的示例测试(Example Tests)。Java拥有极为成熟的开源解决方案,从老牌的 JUnit、TestNG,到支持行为驱动开发(BDD)的 Cucumber,为企业级开发提供了极高的灵活性。Dart官方提供的 test 包(以及 Flutter 场景下的 flutter_test)深度集成在 Dart SDK 中,提供了从基础断言到异步流验证的完整解决方案。

下面我们就来编写第一个Dart单元测试用例。

环境准备

首先是用如下命令添加 test 包:

dart pub add dev:test

该命令执行完后,在 pubspec.yaml 文件中将会有如下开发依赖:

dev_dependencies:
  test: ^1.28.0

注:dev_dependencies下的包仅在开发阶段使用。

当然我们也可以先编辑 pubspec.yaml ,然后 使用 dart pub get 安装 test 包。

接下来我们将用 测试驱动开发(TDD,Test-Driven Development) 实现一个简单的分数(Fraction)类。即将创建的文件包括:

lib
├── fraction.dart
test
└── fraction_test.dart

Fraction 骨架实现

我们使用 int 作为分子、分母的数据类型,来编写 Fraction (分数),要求它自动进行约分。目前不实现任何具体的逻辑,只要求代码能通过编译。

Fraction 骨架代码

// fraction.dart
class Fraction {
  final int num;
  final int den;

  // 构造函数:留空(暂不处理约分和零分母)
  Fraction(this.num, this.den);

  // 运算符重载:全部返回一个占位结果
  Fraction operator +(Fraction other) => Fraction(0, 1);
  Fraction operator -(Fraction other) => Fraction(0, 1);
  Fraction operator *(Fraction other) => Fraction(0, 1);
  Fraction operator /(Fraction other) => Fraction(0, 1);

  @override
  bool operator ==(Object other) => false;

  @override
  int get hashCode => 0;

  @override
  String toString() => "";
}

编写单元测试(测试先行)

// fraction_test.dart
import 'package:hellodart/fraction.dart';
import 'package:test/test.dart';

void main() {
  group('Fraction 构造与约分测试', () {
    test('基本约分:2/4 应该自动变为 1/2', () {
      final f = Fraction(2, 4);
      expect(f.num, 1);
      expect(f.den, 2);
    });

    test('负号规范化:1/-3 应该变为 -1/3', () {
      final f = Fraction(1, -3);
      expect(f.num, -1);
      expect(f.den, 3);
    });

    test('分母为零应抛出异常', () {
      expect(() => Fraction(1, 0), throwsArgumentError);
    });
  });

  group('算术运算测试', () {
    final f12 = Fraction(1, 2);
    final f14 = Fraction(1, 4);

    test('加法验证:1/2 + 1/4 = 3/4', () {
      expect(f12 + f14, Fraction(3, 4));
    });

    test('减法验证:1/2 - 1/4 = 1/4', () {
      expect(f12 - f14, Fraction(1, 4));
    });

    test('乘法验证:1/2 * 1/4 = 1/8', () {
      expect(f12 * f14, Fraction(1, 8));
    });

    test('除法验证:(1/2) / (1/4) = 2/1', () {
      expect(f12 / f14, Fraction(2, 1));
    });
  });
}

group函数用于逻辑分组,expect函数用于断言。使用命令 dart test 或 在IDE(如VSCode)中运行该测试(快捷键通常是 F5),发现全红(全部报错):

Expected: <1>
  Actual: <2>

package:matcher              expect
test/fraction_test.dart 9:7  main.<fn>.<fn>
Expected: <-1>
  Actual: <1>

package:matcher               expect
test/fraction_test.dart 15:7  main.<fn>.<fn>
Expected: throws <Instance of 'ArgumentError'>
  Actual: <Closure: () => Fraction>
   Which: returned Fraction:<>

package:matcher               expect
test/fraction_test.dart 20:7  main.<fn>.<fn>
Expected: Fraction:<>
  Actual: Fraction:<>

package:matcher               expect
test/fraction_test.dart 29:7  main.<fn>.<fn>
Expected: Fraction:<>
  Actual: Fraction:<>

package:matcher               expect
test/fraction_test.dart 33:7  main.<fn>.<fn>
Expected: Fraction:<>
  Actual: Fraction:<>

package:matcher               expect
test/fraction_test.dart 37:7  main.<fn>.<fn>
Expected: Fraction:<>
  Actual: Fraction:<>

package:matcher               expect
test/fraction_test.dart 41:7  main.<fn>.<fn>

NOTE: 使用 dart test -h 查看该命令的详细用法。

实现 Fraction (让测试变绿)

根据测试的要求去实现 Fraction 的各个方法:

// fraction.dart
class Fraction {
  late final int num;
  late final int den;

  Fraction(int n, int d) {
    if (d == 0) throw ArgumentError('分母不能为零');

    // 自动约分逻辑
    int common = _gcd(n.abs(), d.abs());
    int sign = (n * d) < 0 ? -1 : 1;

    num = (n.abs() ~/ common) * sign;
    den = d.abs() ~/ common;
  }

  /// 最大公约数
  int _gcd(int a, int b) => b == 0 ? a : _gcd(b, a % b);

  Fraction operator +(Fraction other) =>
      Fraction(num * other.den + other.num * den, den * other.den);

  Fraction operator -(Fraction other) =>
      Fraction(num * other.den - other.num * den, den * other.den);

  Fraction operator *(Fraction other) =>
      Fraction(num * other.num, den * other.den);

  Fraction operator /(Fraction other) =>
      Fraction(num * other.den, den * other.num);

  @override
  bool operator ==(Object other) =>
      other is Fraction && num == other.num && den == other.den;

  @override
  int get hashCode => Object.hash(num, den);

  @override
  String toString() => '$num/$den';
}

再次运行 fraction_test.dart,已全部通过测试(全绿)。

$ dart test
00:00 +0: test/fraction_test.dart: Fraction 构造与约分测试 基本约分:2/4 应该自动变为 1/2                         
00:00 +3: test/fraction_test.dart: Fraction 构造与约分测试 分母为零应抛出异常                                     
00:00 +4: test/fraction_test.dart: Fraction 构造与约分测试 分母为零应抛出异常                                     
00:00 +4: test/fraction_test.dart: 算术运算测试 加法验证:1/2 + 1/4 = 3/4                                         
00:00 +5: test/fraction_test.dart: 算术运算测试 加法验证:1/2 + 1/4 = 3/4                                         
00:00 +5: test/fraction_test.dart: 算术运算测试 减法验证:1/2 - 1/4 = 1/4                                         
00:00 +6: test/fraction_test.dart: 算术运算测试 减法验证:1/2 - 1/4 = 1/4                                         
00:00 +6: test/fraction_test.dart: 算术运算测试 乘法验证:1/2 * 1/4 = 1/8                                         
00:00 +7: test/fraction_test.dart: 算术运算测试 乘法验证:1/2 * 1/4 = 1/8                                         
00:00 +7: test/fraction_test.dart: 算术运算测试 除法验证:(1/2) / (1/4) = 2/1                                     
00:00 +8: test/fraction_test.dart: 算术运算测试 除法验证:(1/2) / (1/4) = 2/1                                     
00:00 +8: All tests passed!  

结束语

TDD的基本流程:红(测试失败) -> 绿(代码实现) -> 重构。TDD的好处在于:

  • 需求明确化:在写 expect(f.num, 1) 时,我们才真正确定了构造函数必须具备约分功能。
  • 接口契约:我们先写了 f12 + f14,这强迫我们决定 加法操作符参数类型返回类型
  • 重构信心:假如将来我们把 int 改成 BigInt 以支持天文数字计算时,只要运行这一套现成的单元测试,就能立即知道 重构是否破坏了原有逻辑

Dart 的 test 包非常强大,本文仅演示了基本的分组(group)和 断言(expect)功能,接下来将逐步介绍更多高级功能,包括动态匹配器、异步流验证以及强大的 Mock 机制。

Reference