跳到主内容

点击、拖动和输入文本

如何测试用户交互的 Widget。

许多 Widget 不仅显示信息,还会响应用户交互。这包括可以点击的按钮,以及 TextField 用于输入文本。

要测试这些交互,您需要在测试环境中模拟它们。为此,请使用 WidgetTester 库。

WidgetTester 提供了用于输入文本、点击和拖动的函数。

在许多情况下,用户交互会更新应用程序的状态。在测试环境中,Flutter 不会自动在状态更改时重建 Widget。为了确保在模拟用户交互后重建 Widget 树,请调用 WidgetTester 提供的 pump()pumpAndSettle() 函数。本教程使用以下步骤

  1. 创建一个用于测试的 Widget。
  2. 在文本字段中输入文本。
  3. 确保点击按钮会添加待办事项。
  4. 确保滑动删除会移除待办事项。

1. 创建一个用于测试的 Widget

#

对于这个例子,创建一个基本的待办事项应用程序来测试三个功能

  1. 将文本输入到 TextField 中。
  2. 点击 FloatingActionButton 将文本添加到待办事项列表中。
  3. 滑动删除从列表中移除项目。

为了专注于测试,本教程不会提供有关如何构建待办事项应用程序的详细指南。要了解有关此应用程序构建方式的更多信息,请参阅相关教程

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

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

class _TodoListState extends State<TodoList> {
  static const _appTitle = 'Todo List';
  final todos = <String>[];
  final controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _appTitle,
      home: Scaffold(
        appBar: AppBar(title: const Text(_appTitle)),
        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: (direction) => todos.removeAt(index),
                    background: Container(color: Colors.red),
                    child: ListTile(title: Text(todo)),
                  );
                },
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              todos.add(controller.text);
              controller.clear();
            });
          },
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

2. 在文本字段中输入文本

#

现在您已经有了待办事项应用程序,开始编写测试。首先将文本输入到 TextField 中。

通过以下方式完成此任务

  1. 在测试环境中构建 Widget。
  2. 使用 WidgetTesterenterText() 函数。
dart
testWidgets('Add and remove a todo', (tester) async {
  // Build the widget
  await tester.pumpWidget(const TodoList());

  // Enter 'hi' into the TextField.
  await tester.enterText(find.byType(TextField), 'hi');
});

3. 确保点击按钮会添加待办事项

#

在将文本输入到 TextField 后,确保点击 FloatingActionButton 会将项目添加到列表中。

这涉及三个步骤

  1. 使用 WidgetControllertap() 函数点击添加按钮。
  2. 使用 WidgetTesterpump() 函数在状态更改后重建 Widget。
  3. 确保列表项出现在屏幕上。
dart
testWidgets('Add and remove a todo', (tester) async {
  // Enter text code...

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

  // Rebuild the widget after the state has changed.
  await tester.pump();

  // Expect to find the item on screen.
  expect(find.text('hi'), findsOneWidget);
});

4. 确保滑动删除会移除待办事项

#

最后,确保执行待办事项项上的滑动删除操作会将其从列表中移除。这涉及三个步骤

  1. 使用 WidgetControllerdrag() 函数执行滑动删除操作。
  2. 使用 WidgetTesterpumpAndSettle() 函数持续重建 Widget 树,直到完成删除动画。
  3. 确保该项目不再出现在屏幕上。
dart
testWidgets('Add and remove a todo', (tester) async {
  // Enter text and add the item...

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

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

  // Ensure that the item is no longer on screen.
  expect(find.text('hi'), findsNothing);
});

完整示例

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

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

    // Enter 'hi' into the TextField.
    await tester.enterText(find.byType(TextField), 'hi');

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

    // Rebuild the widget with the new item.
    await tester.pump();

    // Expect to find the item on screen.
    expect(find.text('hi'), findsOneWidget);

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

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

    // Ensure that the item is no longer on screen.
    expect(find.text('hi'), findsNothing);
  });
}

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

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

class _TodoListState extends State<TodoList> {
  static const _appTitle = 'Todo List';
  final todos = <String>[];
  final controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _appTitle,
      home: Scaffold(
        appBar: AppBar(title: const Text(_appTitle)),
        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: (direction) => todos.removeAt(index),
                    background: Container(color: Colors.red),
                    child: ListTile(title: Text(todo)),
                  );
                },
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              todos.add(controller.text);
              controller.clear();
            });
          },
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}