flutter-add-widget-test

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Writing Flutter Widget Tests

编写Flutter组件测试

Contents

目录

Setup & Configuration

设置与配置

Ensure the testing environment is properly configured before authoring widget tests.
  1. Add the
    flutter_test
    dependency to the
    dev_dependencies
    section of
    pubspec.yaml
    .
  2. Place all test files in the
    test/
    directory at the root of the project.
  3. Suffix all test file names with
    _test.dart
    (e.g.,
    widget_test.dart
    ).
在编写组件测试前,请确保测试环境已正确配置。
  1. flutter_test
    依赖添加到
    pubspec.yaml
    dev_dependencies
    部分。
  2. 将所有测试文件放在项目根目录的
    test/
    文件夹中。
  3. 所有测试文件的文件名以
    _test.dart
    结尾(例如:
    widget_test.dart
    )。

Core Components

核心组件

Utilize the following
flutter_test
components to interact with and validate the widget tree:
  • WidgetTester
    : The primary interface for building and interacting with widgets in the test environment. Provided automatically by the
    testWidgets()
    function.
  • Finder
    : Locates widgets in the test environment (e.g.,
    find.text('Submit')
    ,
    find.byType(TextField)
    ,
    find.byKey(Key('submit_btn'))
    ).
  • Matcher
    : Verifies the presence or state of widgets located by a
    Finder
    (e.g.,
    findsOneWidget
    ,
    findsNothing
    ,
    findsNWidgets(2)
    ,
    matchesGoldenFile
    ).
使用以下
flutter_test
组件来与组件树交互并进行验证:
  • WidgetTester
    :测试环境中用于构建和交互组件的主要接口,由
    testWidgets()
    函数自动提供。
  • Finder
    :在测试环境中定位组件(例如:
    find.text('Submit')
    find.byType(TextField)
    find.byKey(Key('submit_btn'))
    )。
  • Matcher
    :验证由
    Finder
    定位的组件的存在或状态(例如:
    findsOneWidget
    findsNothing
    findsNWidgets(2)
    matchesGoldenFile
    )。

Workflow: Implementing a Widget Test

工作流程:实现组件测试

Copy the following checklist to track progress when implementing a new widget test.
复制以下检查清单,在实现新组件测试时跟踪进度。

Task Progress

任务进度

  • Step 1: Define the test. Use
    testWidgets('description', (WidgetTester tester) async { ... })
    .
  • Step 2: Build the widget. Call
    await tester.pumpWidget(MyWidget())
    to render the UI. Wrap the widget in a
    MaterialApp
    or
    Directionality
    widget if it requires inherited directional or theme data.
  • Step 3: Locate elements. Instantiate
    Finder
    objects for the target widgets.
  • Step 4: Verify initial state. Use
    expect(finder, matcher)
    to validate the initial render.
  • Step 5: Simulate interactions. Execute gestures or inputs (e.g.,
    await tester.tap(buttonFinder)
    ).
  • Step 6: Rebuild the tree. Call
    await tester.pump()
    or
    await tester.pumpAndSettle()
    to process state changes.
  • Step 7: Verify updated state. Use
    expect()
    to validate the UI after the interaction.
  • Step 8: Run and validate. Execute
    flutter test test/your_test_file_test.dart
    .
  • Step 9: Feedback Loop. Review test output -> identify failing matchers -> adjust widget logic or test assertions -> re-run until passing.
  • 步骤1:定义测试。 使用
    testWidgets('description', (WidgetTester tester) async { ... })
  • 步骤2:构建组件。 调用
    await tester.pumpWidget(MyWidget())
    渲染UI。如果组件需要继承方向或主题数据,请将其包裹在
    MaterialApp
    Directionality
    组件中。
  • 步骤3:定位元素。 为目标组件实例化
    Finder
    对象。
  • 步骤4:验证初始状态。 使用
    expect(finder, matcher)
    验证初始渲染结果。
  • 步骤5:模拟交互。 执行手势或输入操作(例如:
    await tester.tap(buttonFinder)
    )。
  • 步骤6:重建组件树。 调用
    await tester.pump()
    await tester.pumpAndSettle()
    处理状态变化。
  • 步骤7:验证更新后的状态。 使用
    expect()
    验证交互后的UI状态。
  • 步骤8:运行并验证。 执行
    flutter test test/your_test_file_test.dart
    命令。
  • 步骤9:反馈循环。 查看测试输出 → 识别失败的匹配器 → 调整组件逻辑或测试断言 → 重新运行直至通过。

Interaction & State Management

交互与状态管理

Apply the following conditional logic based on the type of interaction or state change being tested:
  • If testing static rendering: Call
    await tester.pumpWidget()
    once, then immediately run
    expect()
    assertions.
  • If testing standard state changes (e.g., button taps):
    1. Call
      await tester.tap(finder)
      .
    2. Call
      await tester.pump()
      to trigger a single frame rebuild.
  • If testing animations, transitions, or asynchronous UI updates:
    1. Trigger the action (e.g.,
      await tester.drag(finder, Offset(500, 0))
      ).
    2. Call
      await tester.pumpAndSettle()
      to repeatedly pump frames until no more frames are scheduled (animation completes).
  • If testing text input: Call
    await tester.enterText(textFieldFinder, 'Input string')
    .
  • If testing items in a dynamic or long list: Call
    await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder)
    to ensure the target widget is rendered before interacting with it.
根据测试的交互类型或状态变化应用以下条件逻辑:
  • 如果测试静态渲染: 调用一次
    await tester.pumpWidget()
    ,然后立即运行
    expect()
    断言。
  • 如果测试标准状态变化(如按钮点击):
    1. 调用
      await tester.tap(finder)
    2. 调用
      await tester.pump()
      触发单帧重建。
  • 如果测试动画、过渡或异步UI更新:
    1. 触发操作(例如:
      await tester.drag(finder, Offset(500, 0))
      )。
    2. 调用
      await tester.pumpAndSettle()
      重复触发帧渲染,直至没有更多待调度帧(动画完成)。
  • 如果测试文本输入: 调用
    await tester.enterText(textFieldFinder, 'Input string')
  • 如果测试动态或长列表中的项: 调用
    await tester.scrollUntilVisible(itemFinder, 500.0, scrollable: listFinder)
    ,确保目标组件在交互前已渲染。

Examples

示例

High-Fidelity Widget Test Implementation

高保真组件测试实现

Target Widget (
lib/todo_list.dart
):
dart
import 'package:flutter/material.dart';

class TodoList extends StatefulWidget {
  const TodoList({super.key});

  
  State<TodoList> createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  final todos = <String>[];
  final controller = TextEditingController();

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            TextField(controller: controller),
            Expanded(
              child: ListView.builder(
                itemCount: todos.length,
                itemBuilder: (context, index) {
                  final todo = todos[index];
                  return Dismissible(
                    key: Key('$todo$index'),
                    onDismissed: (_) => setState(() => todos.removeAt(index)),
                    child: ListTile(title: Text(todo)),
                  );
                },
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              todos.add(controller.text);
              controller.clear();
            });
          },
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
Test Implementation (
test/todo_list_test.dart
):
dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/todo_list.dart';

void main() {
  testWidgets('Add and remove a todo item', (WidgetTester tester) async {
    // 1. Build the widget
    await tester.pumpWidget(const TodoList());

    // 2. Verify initial state
    expect(find.byType(ListTile), findsNothing);

    // 3. Enter text into the TextField
    await tester.enterText(find.byType(TextField), 'Buy groceries');

    // 4. Tap the add button
    await tester.tap(find.byType(FloatingActionButton));

    // 5. Rebuild the widget to reflect the new state
    await tester.pump();

    // 6. Verify the item was added
    expect(find.text('Buy groceries'), findsOneWidget);

    // 7. Swipe the item to dismiss it
    await tester.drag(find.byType(Dismissible), const Offset(500, 0));

    // 8. Build the widget until the dismiss animation ends
    await tester.pumpAndSettle();

    // 9. Verify the item was removed
    expect(find.text('Buy groceries'), findsNothing);
  });
}
目标组件(
lib/todo_list.dart
):
dart
import 'package:flutter/material.dart';

class TodoList extends StatefulWidget {
  const TodoList({super.key});

  
  State<TodoList> createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  final todos = <String>[];
  final controller = TextEditingController();

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Column(
          children: [
            TextField(controller: controller),
            Expanded(
              child: ListView.builder(
                itemCount: todos.length,
                itemBuilder: (context, index) {
                  final todo = todos[index];
                  return Dismissible(
                    key: Key('$todo$index'),
                    onDismissed: (_) => setState(() => todos.removeAt(index)),
                    child: ListTile(title: Text(todo)),
                  );
                },
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              todos.add(controller.text);
              controller.clear();
            });
          },
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
测试实现(
test/todo_list_test.dart
):
dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/todo_list.dart';

void main() {
  testWidgets('Add and remove a todo item', (WidgetTester tester) async {
    // 1. Build the widget
    await tester.pumpWidget(const TodoList());

    // 2. Verify initial state
    expect(find.byType(ListTile), findsNothing);

    // 3. Enter text into the TextField
    await tester.enterText(find.byType(TextField), 'Buy groceries');

    // 4. Tap the add button
    await tester.tap(find.byType(FloatingActionButton));

    // 5. Rebuild the widget to reflect the new state
    await tester.pump();

    // 6. Verify the item was added
    expect(find.text('Buy groceries'), findsOneWidget);

    // 7. Swipe the item to dismiss it
    await tester.drag(find.byType(Dismissible), const Offset(500, 0));

    // 8. Build the widget until the dismiss animation ends
    await tester.pumpAndSettle();

    // 9. Verify the item was removed
    expect(find.text('Buy groceries'), findsNothing);
  });
}