4、Matchers匹配器详解:常用Matchers、自定义Matchers、组合匹配器
测试写多了,你会发现一个规律:断言写得好不好,直接决定了测试的维护成本。而Flutter测试里的断言,核心就是Matchers匹配器。
说白了,Matchers就是一套「检查工具」。它帮你判断某个值是不是你想要的。我刚开始写测试时,只会用equals(),后来发现这玩意儿远比我以为的强大。
4.1 常用Matchers:你每天都在用的那些
先列几个我项目里用得最多的。你想想看,是不是也这样?
| Matcher | 用途 | 示例 |
|---|---|---|
equals() |
判断相等 | expect(result, equals(42)) |
isNull / isNotNull |
判空 | expect(obj, isNull) |
isTrue / isFalse |
布尔判断 | expect(flag, isTrue) |
greaterThan() |
数值比较 | expect(age, greaterThan(18)) |
contains() |
包含判断 | expect(list, contains('Flutter')) |
hasLength() |
长度判断 | expect(items, hasLength(3)) |
TypeMatcher |
类型判断 | expect(obj, isA |
嗯,这里要注意:equals()用的是==运算符。如果你自定义了==,那它就会走你的逻辑。我遇到过有人重写了==但没重写hashCode,结果测试跑得莫名其妙——这种坑,踩一次就够了。
个人习惯:我一般把equals()和isA()搭配着用。比如判断一个对象是不是某个类型,并且值也对。这样测试更严谨。
4.2 自定义Matchers:当内置的不够用时
内置Matchers虽然多,但总有不够用的时候。比如你要检查一个Widget的字体颜色是不是红色,或者一个列表是不是按某种规则排序。这时候就得自己写Matcher了。
自定义Matcher其实很简单。你只需要继承Matcher类,实现两个方法:matches()和describeTo()。
class IsRedColor extends Matcher {
@override
bool matches(dynamic item, Map matchState) {
if (item is Color) {
return item.value == Colors.red.value;
}
return false;
}
@override
Description describeTo(Description description) {
return description.add('是一个红色');
}
}
// 使用
expect(widgetColor, IsRedColor());
你看,核心逻辑就在matches()里。返回true就算匹配成功,false就失败。describeTo()是干嘛的?它用来生成失败时的错误信息。如果你不写,报错时你会看到一堆不知所云的描述。
避坑指南:我曾经写过一个自定义Matcher,忘了实现describeTo()。结果测试失败时,错误信息只有「Expected: 一个匹配器,Actual: 某个值」。排查了半天才发现是Matcher的问题。所以,一定要把描述写清楚。
如果你觉得继承类太麻烦,也可以用predicate()函数。它接受一个匿名函数,快速生成Matcher:
expect(value, predicate((v) => v > 0 && v < 100, '一个0到100之间的数'));
嗯,这种方式适合简单场景。复杂逻辑还是老老实实写类吧。
4.3 组合匹配器:把多个条件串起来
有时候一个条件不够,得同时满足好几个。比如:值不能为空,而且必须大于0,还得是整数。这时候组合匹配器就派上用场了。
Flutter测试框架提供了几个好用的组合器:
allOf():所有条件都满足才算通过。相当于「且」。anyOf():只要满足一个就算通过。相当于「或」。isNot():取反。相当于「非」。
举个例子:
expect(result, allOf([
isNotNull,
greaterThan(0),
isA<int>()
]));
expect(statusCode, anyOf([
equals(200),
equals(201),
equals(204)
]));
expect(error, isNot(isNull));
我个人特别喜欢allOf()。为什么?因为它让测试意图变得非常清晰。你一看就知道「哦,这个值必须同时满足这三个条件」。比写一堆嵌套的expect()要干净得多。
注意:组合匹配器虽然好用,但别滥用。如果一个allOf()里塞了七八个条件,那还不如拆成多个expect()。测试的可读性比「少写几行代码」重要得多。
4.4 实战:一个完整的测试案例
说了这么多,来一个实际场景。假设你有一个用户登录的ViewModel,登录成功后返回一个User对象。你想验证:
- 返回的对象不为空
- 用户名是「admin」
- 角色是
UserRole.admin - 登录时间在今天
用组合匹配器写出来就是这样:
test('登录成功返回正确的用户信息', () async {
final user = await loginViewModel.login('admin', '123456');
expect(user, allOf([
isNotNull,
isA<User>(),
predicate((u) => u.name == 'admin', '用户名为admin'),
predicate((u) => u.role == UserRole.admin, '角色为admin'),
predicate((u) => u.loginTime.day == DateTime.now().day, '登录时间为今天'),
]));
});
你看,一个expect()就把所有条件都覆盖了。如果某个条件失败,错误信息会明确告诉你「是哪个条件没通过」。排查起来非常快。
我的经验:写测试时,我习惯先想「这个值应该满足哪些条件」,然后用allOf()把它们串起来。这样测试逻辑和业务逻辑一一对应,后期维护时一看就懂。
4.5 小结
Matchers匹配器是Flutter测试的基石。常用Matchers帮你搞定80%的场景,自定义Matchers解决剩下的20%,组合匹配器则让多个条件变得优雅。
记住一句话:好的测试,读起来像文档。而Matchers,就是让测试变得像文档的关键工具。
下一章,我们会聊Mock和Stub。到时候你会发现,Matchers和Mock配合起来,威力更大。