单元测试入门: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);
    });
  });
}

分组的好处很明显:

  • 测试报告更清晰,按模块展示
  • 可以给整个组加 setUptearDown(后面会讲)
  • 方便跳过或只运行某个组

我曾经在一个大型项目中,把 200 多个测试用例分成了 15 个组。每次运行测试,控制台输出就像一本书的目录,哪个模块出问题一目了然。

测试描述:写好描述,事半功倍

测试描述不是随便写写的。好的描述应该回答三个问题:

  1. 测试什么?(被测对象)
  2. 什么条件?(输入或状态)
  3. 期望什么?(输出或行为)

举个例子:

❌ 差的描述: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() 组织测试

别小看这些基础。我见过太多人一上来就追求「高级技巧」,结果连最基本的测试都写不好。记住:好的单元测试,首先是可读的、可维护的。

下一章,我们会深入 setUptearDown,看看怎么在测试前后做准备工作。到时候你会发现,测试也可以写得像诗一样优雅。