单元测试入门:test()函数使用、expect断言、分组测试与测试描述
单元测试,说白了就是给代码做「体检」。
我刚开始写 Flutter 时,总觉得测试是浪费时间。直到有一次,一个简单的状态管理 bug 让我排查了整整两天...嗯,从那以后,我再也不敢跳过单元测试了。
test() 函数:测试的最小单元
在 Dart 里,每个测试用例就是一个 test() 函数调用。它的签名很简单:
test('测试描述字符串', () {
// 测试逻辑
});
第一个参数是描述,第二个参数是回调函数。我个人习惯把描述写得像一句话,比如「当用户输入空字符串时,返回 false」。这样测试失败时,你一眼就能看出哪里出了问题。
核心要点:一个 test() 只测一个行为。别想着在一个测试里把所有情况都覆盖了,那样反而会让排查变得困难。
来看个实际例子。假设我们有个验证邮箱的函数:
bool isValidEmail(String email) {
return email.contains('@') && email.contains('.');
}
void main() {
test('有效邮箱返回 true', () {
expect(isValidEmail('test@example.com'), true);
});
}
你想想看,这个测试是不是很直观?描述 + 断言,一目了然。
expect 断言:验证结果的利器
expect() 是测试的核心。它的基本用法是:
expect(实际值, 期望值);
但别以为它只能比相等。我在项目中遇到过很多场景,需要更灵活的断言。Dart 提供了丰富的匹配器(Matcher):
| 匹配器 | 作用 | 示例 |
|---|---|---|
| equals | 判断相等 | expect(result, equals(42)) |
| isNull / isNotNull | 判断空值 | expect(obj, isNull) |
| greaterThan | 大于 | expect(score, greaterThan(60)) |
| contains | 包含子串 | expect(text, contains('hello')) |
| throwsException | 抛出异常 | expect(() => func(), throwsException) |
小技巧:我个人偏爱用 expect(actual, equals(expected)) 而不是 expect(actual, expected)。虽然结果一样,但可读性更好,团队协作时别人能一眼看懂你的意图。
为什么会这样?因为 equals 是显式的,而简写形式有时会让人困惑——你是在比相等,还是在比类型?
分组测试:用 group() 组织你的测试
当测试用例多起来,你就需要分组了。group() 函数可以把相关的测试放在一起:
void main() {
group('验证邮箱功能', () {
test('有效邮箱', () {
expect(isValidEmail('a@b.com'), true);
});
test('无效邮箱 - 缺少@', () {
expect(isValidEmail('ab.com'), false);
});
test('无效邮箱 - 缺少点', () {
expect(isValidEmail('a@bcom'), false);
});
});
}
分组的好处很明显:
- 测试报告更清晰,按模块展示
- 可以给整个组加
setUp和tearDown(后面会讲) - 方便跳过或只运行某个组
我曾经在一个大型项目中,把 200 多个测试用例分成了 15 个组。每次运行测试,控制台输出就像一本书的目录,哪个模块出问题一目了然。
测试描述:写好描述,事半功倍
测试描述不是随便写写的。好的描述应该回答三个问题:
- 测试什么?(被测对象)
- 什么条件?(输入或状态)
- 期望什么?(输出或行为)
举个例子:
❌ 差的描述:test('测试加法', ...)
✅ 好的描述:test('当两个正数相加时,返回它们的和', ...)
你想想看,如果测试失败了,差的描述只能告诉你「加法出问题了」,而好的描述直接告诉你「两个正数相加时出了问题」。排查范围瞬间缩小。
避坑指南:我曾经见过有人把测试描述写成「测试1」「测试2」... 结果三个月后,没人知道这些测试在测什么。最后只能全部删掉重写。嗯,血的教训。
实战:一个完整的测试文件
把上面学到的串起来,写一个完整的测试:
import 'package:test/test.dart';
// 被测函数
int add(int a, int b) => a + b;
void main() {
group('add 函数', () {
test('两个正数相加返回正确结果', () {
expect(add(2, 3), equals(5));
});
test('正数和负数相加', () {
expect(add(5, -3), equals(2));
});
test('两个负数相加', () {
expect(add(-2, -3), equals(-5));
});
test('零加任何数等于该数', () {
expect(add(0, 42), equals(42));
});
});
}
运行这个测试,你会看到类似这样的输出:
00:00 +4: All tests passed!
那个 +4 表示 4 个测试全部通过。如果有失败的,会显示 +3 -1 并告诉你哪个测试挂了。
总结
单元测试入门其实就三件事:
- 用
test()写测试用例 - 用
expect()做断言 - 用
group()组织测试
别小看这些基础。我见过太多人一上来就追求「高级技巧」,结果连最基本的测试都写不好。记住:好的单元测试,首先是可读的、可维护的。
下一章,我们会深入 setUp 和 tearDown,看看怎么在测试前后做准备工作。到时候你会发现,测试也可以写得像诗一样优雅。