Skip to main content

Widget 测试简介

单元测试简介中,你学习了如何使用 test 包测试 Dart 类。要测试 Widget 类,你需要 Flutter SDK 附带的 flutter_test 包提供的几个额外工具。

flutter_test 包提供了以下用于测试 Widget 的工具:

  • WidgetTester 允许在测试环境中构建和与 Widget 交互。
  • testWidgets() 函数为每个测试用例自动创建一个新的 WidgetTester,并用于替换普通的 test() 函数。
  • Finder 类允许在测试环境中搜索 Widget。
  • Widget 特定的 Matcher 常量有助于验证 Finder 是否在测试环境中定位到一个或多个 Widget。

如果这听起来让人不知所措,别担心。在本教程中,你将学习所有这些部分是如何组合在一起的,它包含以下步骤:

  1. 添加 flutter_test 依赖项。
  2. 创建要测试的 Widget。
  3. 创建 testWidgets 测试。
  4. 使用 WidgetTester 构建 Widget。
  5. 使用 Finder 搜索 Widget。
  6. 使用 Matcher 验证 Widget。

1. 添加 flutter_test 依赖项

#

在编写测试之前,请在 pubspec.yaml 文件的 dev_dependencies 部分包含 flutter_test 依赖项。如果使用命令行工具或代码编辑器创建新的 Flutter 项目,则此依赖项应该已经存在。

yaml
dev_dependencies:
  flutter_test:
    sdk: flutter

2. 创建要测试的 Widget

#

接下来,创建一个 Widget 用于测试。在本教程中,创建一个显示 titlemessage 的 Widget。

dart
class MyWidget extends StatelessWidget {
  const MyWidget({
    super.key,
    required this.title,
    required this.message,
  });

  final String title;
  final String message;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Text(message),
        ),
      ),
    );
  }
}

3. 创建 testWidgets 测试

#

有了要测试的 Widget 后,开始编写你的第一个测试。使用 flutter_test 包提供的 testWidgets() 函数来定义测试。testWidgets 函数允许你定义 Widget 测试并创建一个 WidgetTester 来使用。

此测试验证 MyWidget 是否显示给定的标题和消息。它已相应命名,将在下一节中填充。

dart
void main() {
  // 定义一个测试。TestWidgets 函数也提供了一个 WidgetTester 来使用。
  // WidgetTester 允许你在测试环境中构建和与 Widget 交互。
  testWidgets('MyWidget has a title and message', (tester) async {
    // 测试代码写在这里。
  });
}

4. 使用 WidgetTester 构建 Widget

#

接下来,使用 WidgetTester 提供的 pumpWidget() 方法在测试环境中构建 MyWidgetpumpWidget 方法构建并呈现提供的 Widget。

创建一个 MyWidget 实例,显示 "T" 作为标题,"M" 作为消息。

dart
void main() {
  testWidgets('MyWidget has a title and message', (tester) async {
    // 通过告诉测试器构建它来创建 Widget。
    await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
  });
}

关于 pump() 方法的说明

#

在初始调用 pumpWidget() 之后,WidgetTester 提供了重建相同 Widget 的其他方法。如果你正在使用 StatefulWidget 或动画,这将非常有用。

例如,点击按钮会调用 setState(),但 Flutter 不会自动在测试环境中重建你的 Widget。使用以下方法之一来请求 Flutter 重建 Widget。

tester.pump(Duration duration)
安排一个帧并触发 Widget 的重建。如果指定了 Duration,它会将时钟提前该量并安排一个帧。即使持续时间长于单个帧,它也不会安排多个帧。
tester.pumpAndSettle()
使用给定的持续时间重复调用 pump(),直到不再安排任何帧。这实际上是在等待所有动画完成。

这些方法提供了对构建生命周期的细粒度控制,这在测试时特别有用。

5. 使用 Finder 搜索我们的 Widget

#

在测试环境中有了 Widget 后,使用 Finder 在 Widget 树中搜索 titlemessage 文本 Widget。这允许验证 Widget 是否正在正确显示。

为此,使用 flutter_test 包提供的顶级 find() 方法来创建 Finder。由于你知道你在寻找 Text Widget,因此使用 find.text() 方法。

有关 Finder 类的更多信息,请参阅在 Widget 测试中查找 Widget 教程。

dart
void main() {
  testWidgets('MyWidget has a title and message', (tester) async {
    await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));

    // 创建 Finder。
    final titleFinder = find.text('T');
    final messageFinder = find.text('M');
  });
}

6. 使用 Matcher 验证 Widget

#

最后,使用 flutter_test 提供的 Matcher 常量验证标题和消息 Text Widget 是否出现在屏幕上。Matcher 类是 test 包的核心部分,并提供了一种通用的方法来验证给定值是否符合预期。

确保 Widget 恰好出现一次。为此,使用 findsOneWidget Matcher

dart
void main() {
  testWidgets('MyWidget has a title and message', (tester) async {
    await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
    final titleFinder = find.text('T');
    final messageFinder = find.text('M');

    // 使用 flutter_test 提供的 `findsOneWidget` matcher 来验证
    // Text Widget 是否在 Widget 树中恰好出现一次。
    expect(titleFinder, findsOneWidget);
    expect(messageFinder, findsOneWidget);
  });
}

其他 Matcher

#

除了 findsOneWidget 之外,flutter_test 还为常见情况提供了其他 matcher。

findsNothing
验证没有找到 Widget。
findsWidgets
验证找到一个或多个 Widget。
findsNWidgets
验证找到特定数量的 Widget。
matchesGoldenFile
验证 Widget 的渲染是否与特定位图图像(“黄金文件”测试)匹配。

##完整示例

dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  // 定义一个测试。TestWidgets 函数也提供了一个 WidgetTester 来使用。
  // WidgetTester 允许你在测试环境中构建和与 Widget 交互。
  testWidgets('MyWidget has a title and message', (tester) async {
    // 通过告诉测试器构建它来创建 Widget。
    await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));

    // 创建 Finder。
    final titleFinder = find.text('T');
    final messageFinder = find.text('M');

    // 使用 flutter_test 提供的 `findsOneWidget` matcher 来
    // 验证 Text Widget 是否在 Widget 树中恰好出现一次。
    expect(titleFinder, findsOneWidget);
    expect(messageFinder, findsOneWidget);
  });
}

class MyWidget extends StatelessWidget {
  const MyWidget({
    super.key,
    required this.title,
    required this.message,
  });

  final String title;
  final String message;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Text(message),
        ),
      ),
    );
  }
}