Skip to content

单元测试

test 提供了在 dart 中编写和运行测试的方法

$ dart pub get test --dev

编写测试

测试使用顶级的 test() 函数, 测试断言使用 expect()

dart
import 'package:test/test.dart';

void main() {
  test('String.trim() removes surrounding whitespace', () {
    var string = '  foo ';
    expect(string.trim(), equals('foo'));
  });
}

运行结果:

00:00 +0: String.trim() removes surrounding whitespace
00:00 +1: All tests passed!

测试支持分组函数 group() , 每个分组的描述都会添加到测试描述的开头

dart
import 'package:test/test.dart';

void main() {
  group('int', () {
    test('.remainder() returns the remainder of division', () {
      expect(11.remainder(3), equals(2));
    });

    test('.toRadixString() returns a hex string', () {
      expect(11.toRadixString(16), equals('b'));
    });
  });
}

运行结果:

00:00 +0: int .remainder() returns the remainder of division
00:00 +1: int .toRadixString() returns a hex string
00:00 +2: All tests passed!

任何来自 matcher 包的匹配器都可以与 expect() 一起使用来进行复杂的验证:

dart
import 'package:test/test.dart';

void main() {
  test('.split() splits the string on the delimiter', () {
    expect('foo,bar,baz',
        allOf([contains('foo'), isNot(startsWith('bar')), endsWith('baz')]));
  });
}

运行结果:

00:00 +0: .split() splits the string on the delimiter
00:00 +1: All tests passed!

同时还还可以使用 throwsA() 函数或 throwsFormatException 等匹配器来测试异常:

dart
import 'package:test/test.dart';

void main() {
  test('.parse() fails on invalid input', () {
    expect(() => int.parse('X'), throwsFormatException);
  });
}

运行结果:

00:00 +0: .parse() fails on invalid input
00:00 +1: All tests passed!

可以使用 setUp()tearDown() 函数在测试之间处理/关闭共享代码。 setUp() 回调将在组或测试套件中的每个测试之前运行, tearDown() 将在测试之后运行。即使测试失败, tearDown() 也将运行,以确保正确清理

dart
import 'package:test/test.dart';

void main() {
  late HttpServer server;
  late Uri url;
  setUp(() async {
    server = await HttpServer.bind('localhost', 0);
    url = Uri.parse('http://${server.address.host}:${server.port}');
  });

  tearDown(() async {
    await server.close(force: true);
    server = null;
    url = null;
  });

  // ...
}

运行测试

可以使用 dart test path/to/test.dart 在单个测试文件上运行(Dart 2.10 以上版本 - 以前的 sdk 版本必须使用 pub run test 代替 dart test )。

可以使用 dart test path/to/dir 同时运行多个测试。

也可以通过使用 dart path/to/test.dart 调用来仅在 Dart VM 上运行测试,但这样不会加载完整的测试运行器,并且会缺少一些功能。

测试运行器将任何以 _test.dart 结尾的文件视为测试文件。如果不传递任何路径,它将运行 test/ 目录中的所有测试文件,可以一次性测试整个应用程序

可以使用 dart test -n "test name" 通过名称选择要运行的特定测试用例。将字符串解释为正则表达式,并且只会运行描述(包括任何组描述)与该正则表达式匹配的测试。另外还可以使用 -N 标志来运行名称包含纯文本字符串的测试。

默认情况下,测试在 Dart VM 中运行,但也可以通过传递 dart test -p chrome path/to/test.dart 在浏览器中运行它们. test 会负责启动浏览器并加载测试,所有结果都将在命令行上报告,就像 VM 测试一样。实际上,甚至可以使用单个命令在两个平台上运行测试: dart test -p "chrome,vm" path/to/test.dart

测试路径查询

支持测试路径上的一些查询参数,这些参数允许你在这些路径内仅过滤要运行的测试。这些过滤器与传递的任何全局选项合并,并且所有过滤器都必须匹配才能运行测试。

  • name:与 - -name - 相同(简单的包含检查)。 这是唯一支持多个条目的选项
  • full-name:对测试名称要求完全匹配
  • line:匹配测试套件中来自此行的任何测试
  • col:匹配测试套件中来自此列的任何测试

示例用法: dart test "path/to/test.dart?line=10&col=2"

行/列匹配语义

line 和 col 过滤器针对从调用测试函数的当前堆栈跟踪进行匹配,如果跟踪中的任何帧都满足以下所有条件,则视为匹配:

  • URI 地址 与根测试套件 uri 匹配, 这意味着它不会与导入的库中的行匹配。
  • 如果传递了 line 和 col,则必须都与 - 同一帧(WIP) - 匹配
  • 要匹配的特定行和列由创建堆栈跟踪的工具定义。这通常意味着它们是基于 1 的而不是基于 0 的,但这个包括不控制确切的语义,它们可能会因平台实现而变化。

分片测试

还可以使用 --total-shards--shard-index 参数将测试分片,从而将测试套件拆分并分开运行。例如,如果想要运行测试套件的 3 个分片,可以如下运行它们:

dart test --total-shards 3 --shard-index 0 path/to/test.dart
dart test --total-shards 3 --shard-index 1 path/to/test.dart
dart test --total-shards 3 --shard-index 2 path/to/test.dart

这将运行测试套件中的所有测试,但每个分片只会运行其中的一部分。如果有一个大型测试套件并想将测试运行并行化以使其运行得更快,则这很有用。

乱序测试

可以使用 --test-randomize-ordering-seed 参数将测试顺序洗牌。可以使用特定的种子(确定性)或每次运行的随机种子来洗牌测试。例如,考虑以下测试运行:

dart test --test-randomize-ordering-seed=12345
dart test --test-randomize-ordering-seed=random

设置 --test-randomize-ordering-seed=0 与不指定它效果相同,意味着测试顺序将保持不变。

选择测试报告

可以使用 --reporter=<option> 命令行选项调整测试结果的输出格式。默认格式是 compact 紧凑输出格式(单行输出) ,随着测试运行而持续更新。但是,当在 GitHub Actions CI 上运行(通过检查 GITHUB_ACTIONS 环境变量是否为 true 检测到)时,默认格式更改为 github 输出格式 - 为该 CI/CD 系统定制的报告者。

收集代码覆盖率

要收集代码覆盖率,可以使用 --coverage <directory> 参数运行测试。指定的目录可以是绝对路径或相对路径。如果指定路径处不存在目录,将创建目录。如果存在目录并冲突,则会被覆盖

此选项将在套件级别启用代码覆盖率收集,并在指定的目录中输出结果覆盖率文件。然后可以使用 package:coverage format_coverage 文件格式化这些文件

目前仅为在 Dart VM 或 Chrome 上运行的测试实现了覆盖率收集

以下是如何运行测试并将收集的覆盖率格式化为 LCOV 的示例:

官方

## Run Dart tests and output them at directory `./coverage`:
dart run test --coverage=./coverage

## Activate package `coverage` (if needed):
dart pub global activate coverage

## Format collected coverage to LCOV (only for directory "lib")
dart pub global run coverage:format_coverage --packages=.dart_tool/package_config.json --report-on=lib --lcov -o ./coverage/lcov.info -i ./coverage

## Generate LCOV report:
genhtml -o ./coverage/report ./coverage/lcov.info

## Open the HTML coverage report:
open ./coverage/report/index.html

另一个

dart test --coverage coverage
format_coverage -i coverage -o lcov.info --lcov

ps: 图示

限制在特定平台上测试

有些测试文件只有在特定平台上才有意义。它们可能使用 dart:htmldart:io ,可能测试 Windows 的特定文件系统行为,或者可能使用仅在 Chrome 中可用的功能。 @TestOn 注释很容易声明测试文件应在哪些平台上运行。只需在文件顶部放置它,在任何库或导入声明之前:

@TestOn('vm')

import 'dart:io';

import 'package:test/test.dart';

void main() {
  // ...
}

@TestOn 所传递的字符串被称为 平台选择器 ,它指定测试可在哪些平台上运行。它可以是平台名称,也可以是更复杂的像涉及这些平台名称的 Dart 的布尔表达式

还可以通过在包配置文件中添加 test_on 字段来声明整个包只在特定平台上工作

平台选择器

平台选择器使用 boolean_selector 包定义的布尔选择器(boolean selector syntax)语法,这是 Dart 表达式语法的子集,仅支持布尔运算。定义了以下标识符:

  • vm - :测试是否在命令行 Dart VM 上运行。
  • chrome - :测试是否在 Google Chrome 上运行。
  • firefox - :测试是否在 Mozilla Firefox 上运行。
  • safari - :测试是否在 Apple Safari 上运行。
  • ie - :测试是否在 Microsoft Internet Explorer 上运行。
  • node - :测试是否在 Node.js 上运行。
  • dart-vm - :测试是否在任何上下文的 Dart VM 上运行。它与 - !js - 完全相同。
  • browser - :测试是否在任何浏览器中运行。
  • js - :测试是否已编译为 JS。这与 - !dart-vm - 完全相同。
  • blink - :测试是否在使用 Blink 渲染引擎的浏览器中运行。
  • windows - :测试是否在 Windows 上运行。这只有在 vm 或 node 为 true 时才为真。
  • mac-os - :测试是否在 MacOS 上运行。这只有在 vm 或 node 为 true 时才为真。
  • linux - :测试是否在 Linux 上运行。这只有在 vm 或 node 为 true 时才为真。
  • android - :测试是否在 Android 上运行。如果 vm 为 false,则此选项也为 false,这意味着如果测试在 Android 浏览器上运行,则此选项不为真。
  • ios - :测试是否在 iOS 上运行。如果 vm 为 false,则此选项也为 false,这意味着如果测试在 iOS 浏览器上运行,则此选项不为真。
  • posix - :测试是否在 POSIX 操作系统上运行。这相当于 - !windows

例如,如果要在每个浏览器上运行测试,但除了 Chrome,可以写 @TestOn('browser && !chrome')

在 Node.js 上运行测试

测试运行器还支持通过传递 --platform node 将测试编译成 JavaScript 并在 Node.js 上运行。请注意,Node 既无法访问 dart:html 也无法访问 dart:io ,因此将使用 js 包调用任何特定于平台的 API。但是,当测试用于 JavaScript 代码的 API 时,它可能很有用。

测试运行器在系统路径上查找名为 node(在 Mac OS 或 Linux 上)或 node.exe(在 Windows 上)的可执行文件。在编译 Node.js 测试时,它传递 -Dnode=true , 因此测试可以使用 const bool.fromEnvironment('node') 确定是否在 Node 上运行。它还会设置 --server-mode ,这将告诉编译器 dart:html 不可用。