Many of the Widgets we build not only display information, but also respond touser interaction. This includes buttons that users can tap on, dragging itemsacross the screen, or entering text into aTextField.

In order to test these interactions, we need a way to simulate them in the testenvironment. To do so, we can use theWidgetTesterclass provided by theflutter_testlibrary.

The WidgetTester provides methods for entering text, tapping, and dragging.

  • enterText
  • tap
  • dragIn many cases, user interactions will update the state of our app. In the testenvironment, Flutter will not automatically rebuild widgets when the statechanges. To ensure our Widget tree is rebuilt after we simulate a userinteraction, we must call thepump orpumpAndSettlemethods provided by the WidgetTester.

Directions

  • Create a Widget to test
  • Enter text in the text field
  • Ensure tapping a button adds the todo
  • Ensure swipe-to-dismiss removes the todo

1. Create a Widget to test

For this example, we’ll create a basic todo app. It will have three mainfeatures that we’ll want to test:

  1. class TodoList extends StatefulWidget {
  2. @override
  3. _TodoListState createState() => _TodoListState();
  4. }
  5. class _TodoListState extends State<TodoList> {
  6. static const _appTitle = 'Todo List';
  7. final todos = <String>[];
  8. final controller = TextEditingController();
  9. @override
  10. Widget build(BuildContext context) {
  11. return MaterialApp(
  12. title: _appTitle,
  13. home: Scaffold(
  14. appBar: AppBar(
  15. title: Text(_appTitle),
  16. ),
  17. body: Column(
  18. children: [
  19. TextField(
  20. controller: controller,
  21. ),
  22. Expanded(
  23. child: ListView.builder(
  24. itemCount: todos.length,
  25. itemBuilder: (BuildContext context, int index) {
  26. final todo = todos[index];
  27. return Dismissible(
  28. key: Key('$todo$index'),
  29. onDismissed: (direction) => todos.removeAt(index),
  30. child: ListTile(title: Text(todo)),
  31. background: Container(color: Colors.red),
  32. );
  33. },
  34. ),
  35. ),
  36. ],
  37. ),
  38. floatingActionButton: FloatingActionButton(
  39. onPressed: () {
  40. setState(() {
  41. todos.add(controller.text);
  42. controller.clear();
  43. });
  44. },
  45. child: Icon(Icons.add),
  46. ),
  47. ),
  48. );
  49. }
  50. }

2. Enter text in the text field

Now that we have a todo app, we can begin writing our test! In this case, we’llstart by entering text into the TextField.

We can accomplish this task by:

  • Building the Widget in the Test Environment
  • Using the enterText method from the WidgetTester
  1. testWidgets('Add and remove a todo', (WidgetTester tester) async {
  2. // Build the Widget
  3. await tester.pumpWidget(TodoList());
  4. // Enter 'hi' into the TextField
  5. await tester.enterText(find.byType(TextField), 'hi');
  6. });

Note: This recipe builds upon previous Widget testing recipes. To learn thecore concepts of Widget testing, see the following recipes:

3. Ensure tapping a button adds the todo

After we’ve entered text into the TextField, we’ll want to ensure that tappingthe FloatingActionButton adds the item to the list.

This will involve three steps:

  • Tap the add button using thetapmethod
  • Rebuild the Widget after the state has changed using thepumpmethod
  • Ensure the list item appears on screen
  1. testWidgets('Add and remove a todo', (WidgetTester tester) async {
  2. // Enter text code...
  3. // Tap the add button
  4. await tester.tap(find.byType(FloatingActionButton));
  5. // Rebuild the Widget after the state has changed
  6. await tester.pump();
  7. // Expect to find the item on screen
  8. expect(find.text('hi'), findsOneWidget);
  9. });

4. Ensure swipe-to-dismiss removes the todo

Finally, we can ensure that performing a swipe-to-dismiss action on the todoitem will remove it from the list. This will involve three steps:

  • Use the drag method to perform a swipe-to-dismiss action.
  • Use the pumpAndSettle method to continually rebuild our Widget tree until the dismiss animation is complete.
  • Ensure the item no longer appears on screen.
  1. testWidgets('Add and remove a todo', (WidgetTester tester) async {
  2. // Enter text and add the item...
  3. // Swipe the item to dismiss it
  4. await tester.drag(find.byType(Dismissible), Offset(500.0, 0.0));
  5. // Build the Widget until the dismiss animation ends
  6. await tester.pumpAndSettle();
  7. // Ensure the item is no longer on screen
  8. expect(find.text('hi'), findsNothing);
  9. });

Complete example

Once we’ve completed these steps, we should have a working app with a test toensure it works correctly!

  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_test/flutter_test.dart';
  3. void main() {
  4. testWidgets('Add and remove a todo', (WidgetTester tester) async {
  5. // Build the Widget
  6. await tester.pumpWidget(TodoList());
  7. // Enter 'hi' into the TextField
  8. await tester.enterText(find.byType(TextField), 'hi');
  9. // Tap the add button
  10. await tester.tap(find.byType(FloatingActionButton));
  11. // Rebuild the Widget with the new item
  12. await tester.pump();
  13. // Expect to find the item on screen
  14. expect(find.text('hi'), findsOneWidget);
  15. // Swipe the item to dismiss it
  16. await tester.drag(find.byType(Dismissible), Offset(500.0, 0.0));
  17. // Build the Widget until the dismiss animation ends
  18. await tester.pumpAndSettle();
  19. // Ensure the item is no longer on screen
  20. expect(find.text('hi'), findsNothing);
  21. });
  22. }
  23. class TodoList extends StatefulWidget {
  24. @override
  25. _TodoListState createState() => _TodoListState();
  26. }
  27. class _TodoListState extends State<TodoList> {
  28. static const _appTitle = 'Todo List';
  29. final todos = <String>[];
  30. final controller = TextEditingController();
  31. @override
  32. Widget build(BuildContext context) {
  33. return MaterialApp(
  34. title: _appTitle,
  35. home: Scaffold(
  36. appBar: AppBar(
  37. title: Text(_appTitle),
  38. ),
  39. body: Column(
  40. children: [
  41. TextField(
  42. controller: controller,
  43. ),
  44. Expanded(
  45. child: ListView.builder(
  46. itemCount: todos.length,
  47. itemBuilder: (BuildContext context, int index) {
  48. final todo = todos[index];
  49. return Dismissible(
  50. key: Key('$todo$index'),
  51. onDismissed: (direction) => todos.removeAt(index),
  52. child: ListTile(title: Text(todo)),
  53. background: Container(color: Colors.red),
  54. );
  55. },
  56. ),
  57. ),
  58. ],
  59. ),
  60. floatingActionButton: FloatingActionButton(
  61. onPressed: () {
  62. setState(() {
  63. todos.add(controller.text);
  64. controller.clear();
  65. });
  66. },
  67. child: Icon(Icons.add),
  68. ),
  69. ),
  70. );
  71. }
  72. }