第一个自动化测试用例
编写代码只是开发的一半,另一半则是确保这些代码在复杂的生产环境中能够持续稳定地运行。本节先简要介绍一下自动化测试,然后聚焦到单元测试,开始我们的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 机制。