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配合起来,威力更大。