Skip to content

Flutter - 测试

测试是应用程序开发生命周期中的关键阶段,它能确保代码质量、防止回归并增强应用程序可靠性的信心。Flutter 提供了一个全面的测试框架,支持各种级别的测试。

Flutter 将自动化测试分为三种主要类型,形成一个测试金字塔:

  • 单元测试(Unit Tests): 测试单个函数、方法或类。它们验证逻辑的正确性,与 Flutter 框架、UI 渲染或外部服务没有依赖关系。它们执行速度快。使用 test 包。
  • Widget 测试(Widget Tests): 测试单个 Widget。它们验证 Widget 的 UI 是否符合预期,并能正确响应用户输入或事件。Widget 测试运行在比单元测试更真实的环中(使用测试 Flutter 框架实例),但比集成测试快。使用 flutter_test 包。
  • 集成测试(Integration Tests): 测试一个完整的应用或其大部分功能。它们验证应用的不同部分(Widget、逻辑、服务)是否协同工作正常。它们运行在真实设备或模拟器/仿真器上,并自动化用户交互。它们速度最慢,但提供最高的信心。使用 integration_test 包(它利用 flutter_driver 或原生自动化)。

本章主要重点介绍 Widget 测试,这是 Flutter 测试能力中独特且强大的特性。

Widget 测试允许你在测试环境中构建并与 Widget 交互,而无需完整的设备或模拟器。flutter_test 包提供了所需的工具。

Widget 测试使用 testWidgets 函数定义,该函数提供一个 WidgetTester 实例。这个测试器允许你构建、与 Widget 交互并检查它们。

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
// 假设 MyWidget 已在别处定义
// import 'package:my_app/my_widget.dart';
void main() {
testWidgets('MyWidget displays a title and message', (WidgetTester tester) async {
// 测试代码写在这里
});
}

典型的 Widget 测试遵循以下步骤:

  1. 构建 Widget: 使用 tester.pumpWidget() 在模拟屏幕环境中构建并渲染你要测试的 Widget。你通常会将你的 Widget 包裹在 MaterialApp 或 Scaffold 中以提供必要的上下文(如文本方向性、主题)。

  2. 查找 Widget: 使用 Finder 对象在渲染树中查找特定的 Widget。常用的查找器包括:

    • find.text('Some Text'):查找包含特定字符串的 Text Widget。
    • find.byKey(const Key('my-key')):查找具有特定 Key 的 Widget。
    • find.byType(MyWidget):查找特定类型的 Widget。
    • find.byIcon(Icons.add):查找具有特定图标的 Icon Widget。
    • find.widgetWithText(ElevatedButton, 'Submit'):查找包含特定文本的 ElevatedButton。
    • find.descendant(...) / find.ancestor(...):查找相对于其他 Widget 的 Widget。
  3. 验证 Widget: 使用 Matcher 对象和 expect 函数来验证找到的 Widget 是否存在或具有特定属性。常用的匹配器包括:

    • findsOneWidget:验证恰好找到一个 Widget。
    • findsNothing:验证没有找到任何 Widget。
    • findsWidgets:验证找到一个或多个 Widget。
    • findsNWidgets(n):验证恰好找到 n 个 Widget。
    • matchesGoldenFile(...):用于快照测试(验证 Widget 外观)。
  4. (可选)与 Widget 交互: 使用 WidgetTester 方法模拟用户交互:

    • tester.tap(finder):模拟点击 Widget。
    • tester.enterText(finder, 'text'):模拟在文本字段中输入文本。
    • tester.drag(finder, offset):模拟拖动。
    • tester.pump():将时钟向前推进一帧,在状态改变后或用于动画很有用。
    • tester.pumpAndSettle():持续泵送帧直到动画稳定下来。

我们来看一下新 Flutter 项目创建时自带的默认测试文件(test/widget_test.dart),它测试的是计数器应用:

// 这是一个基本的 Flutter Widget 测试。
//
// 要在测试中与 Widget 进行交互,请使用
// flutter_test 包中的 WidgetTester 工具。
// 例如,你可以发送点击和滚动
// 手势。你还可以使用 WidgetTester 在 Widget
// 树中查找子 Widget,读取文本,并验证 Widget 属性的值是否正确。
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart'; // 导入你的主应用文件
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// 构建我们的应用并触发一帧。
await tester.pumpWidget(const MyApp()); // 构建根 Widget
// 验证计数器从 0 开始。
expect(find.text('0'), findsOneWidget); // 查找文本 '0'
expect(find.text('1'), findsNothing); // 确保文本 '1' 不存在
// 点击 '+' 图标并触发一帧。
await tester.tap(find.byIcon(Icons.add)); // 按图标查找按钮并点击它
await tester.pump(); // 状态改变后触发重建
// 验证计数器已增加。
expect(find.text('0'), findsNothing); // 确保文本 '0' 已消失
expect(find.text('1'), findsOneWidget); // 验证文本 '1' 现在存在
// 示例:再次点击并验证 '2'
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsNothing);
expect(find.text('2'), findsOneWidget);
});
}

此测试演示了:

  • 使用 pumpWidget 构建整个应用。
  • 使用 find.text 和 find.byIcon 查找 Widget。
  • 使用 findsOneWidget 和 findsNothing 验证 Widget 的存在/不存在。
  • 使用 tester.tap 模拟点击手势。
  • 在交互后使用 tester.pump 触发 UI 更新。

你可以使用 IDE(如 VS Code、Android Studio)或命令行运行测试:

  • 运行所有测试: flutter test
  • 运行特定文件: flutter test test/my_test_file.dart

IDE 集成通常提供测试结果的可视化反馈,并允许运行单个测试或测试套件。

  • 使用 Key: 为重要的 Widget 添加 Key 属性,以便在测试中更容易、更可靠地查找它们(find.byKey)。
  • 测试 Widget 交互: 验证 Widget 能正确响应点击、文本输入等。
  • 测试不同状态: 确保你的 Widget 在各种条件下(例如加载中、错误、数据为空)都能正确渲染。
  • 保持测试焦点: 每个 testWidgets 代码块理想情况下应只测试一个特定方面或交互。
  • 组织测试: 将测试文件放在 test 目录中,在适用的情况下,目录结构应与 lib 目录结构镜像。

Widget 测试是 Flutter 生态系统中强大的工具,使你能够充满信心地构建健壮可靠的用户界面。