Widget testing
Widget tests are used to verify that individual widgets function as expected. To use widget tests in the project, the flutter_test
dependency needs to be defined under dev_dependencies
of the pubspec.yaml
, as outlined in the previous part.
Testing a simple widget
Let's start by testing a simple widget. Create a file hello_world_widget.dart
to the lib
folder of the project, and place the following contents to the file.
import 'package:flutter/material.dart';
class HelloWorldWidget extends StatelessWidget {
Widget build(BuildContext context) {
return const Text("Hello world!");
}
}
Next, create a file hello_world_widget_test.dart
to the test
folder of the project, and place the following contents to the file.
import 'package:flutter_test/flutter_test.dart';
import '../lib/hello_world_widget.dart';
void main() {
testWidgets("HelloWorldWidget displays 'Hello world!'.", (tester) async {
await tester.pumpWidget(HelloWorldWidget());
final helloWorldFinder = find.text('Hello world!');
expect(helloWorldFinder, findsOneWidget);
});
}
Now, when we run the tests, we see an error. The following output contains some key pieces of the error.
flutter test
00:01 +2: HelloWorldWidget displays 'Hello world!'...
The following assertion was thrown building Text("Hello world!"):
No Directionality widget found.
...
Typically, the Directionality widget is introduced by the MaterialApp or ...
...
The error states that the widget does not provide information on the display direction of the text, and thus, the widget cannot be loaded. The error also suggests that the direction of the text is introduced by the MaterialApp
class.
When testing simple widgets, the widgets still need to provide information on how the contents should be displayed.
To accommodate for this, let's modify our test. Instead of using a directly HelloWorldWidget
, let's create a MaterialApp
and use the HelloWorldWidget
as the home
property for the MaterialApp
. After the modification, the test looks as follows.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../lib/hello_world_widget.dart';
void main() {
testWidgets("HelloWorldWidget displays 'Hello world!'.", (tester) async {
await tester.pumpWidget(MaterialApp(home: HelloWorldWidget()));
final helloWorldFinder = find.text('Hello world!');
expect(helloWorldFinder, findsOneWidget);
});
}
Now, when we run the tests, all the tests pass.
flutter test
00:01 +3: All tests passed!
Structure of a widget test
Each widget test starts with the testWidgets function, which is given the name of the test and an asynchronous test function that receives an instance of a WidgetTester.
// ...
testWidgets("HelloWorldWidget displays 'Hello world!'.", (tester) async {
// ...
A widget test instantiates the widget to be tested by calling the method pumpWidget of the WidgetTester
. The method is given the widget under test as a parameter -- invoking the pumpWidget
function essentially calls the runApp function in the background, rendering the widget for testing.
// ...
await tester.pumpWidget(MaterialApp(home: HelloWorldWidget()));
// ...
Once the widget has been rendered, it can be tested. The call find.text is used to create a finder that can be used to look for a widget with the given text from the application.
// ...
final helloWorldFinder = find.text('Hello world!');
// ...
Finally, once we have initiated the finder, we can define expectations for the widgets that it found. In our case, we used the findOneWidget constant to assess that the finder found exactly one widget matching the criteria.
// ...
expect(helloWorldFinder, findsOneWidget);
// ...
For additional information on the basics of widget testing, check out Flutter's cookbook An introduction to widget testing.
Testing widget with state
Let's next look into testing a widget with a state. Create a file called count_widget.dart
into the folder lib
and place the following code into it.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final countProvider = StateProvider<int>((ref) => 0);
class CountWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final int count = ref.watch(countProvider);
return ElevatedButton(
onPressed: () =>
ref.watch(countProvider.notifier).update((state) => state + 1),
child: Text('$count'),
);
}
}
The above widget creates a button that, when pressed, updates the value shown in the button by one.
Next, create a file called count_widget_test.dart
to the folder test
and place the following code to it.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../lib/count_widget.dart';
void main() {
testWidgets("CountWidget initial value is 0.", (tester) async {
final myApp = ProviderScope(child: MaterialApp(home: CountWidget()));
await tester.pumpWidget(myApp);
final zeroCountFinder = find.text('0');
expect(zeroCountFinder, findsOneWidget);
});
}
When we run the tests, all the tests pass.
flutter test
00:01 +4: All tests passed!
Let's create another test to the file that checks whether the number in the button changes when it is pressed. For pressing an element in the user interface, we use the asynchronous method tap from the tester
(which inherits it from WidgetController). The method tap
is given an element that should be pressed, identified using a finder created with the one of the functions of find
, which is an instance of CommonFinders.
For example, to find a widget with the text
0
and to press it, we would callawait tester.tap(find.text('0'))
.
When we interact with the user interface, we need to provide the user interface the possibility to react to the interactions. Calling the asynchronous method pumpAndSettle of the tester
waits for all the animations to complete. These animations include, e.g., animations related to pressing a button.
After interacting with the user interface, we call
await tester.pumpAndSettle()
.
Once we have waited for the animations to pass, we can look for an expected change. For example, in the above application, pressing the button would lead to the text being 1
.
Together, a test file used for checking that the initial value is 0
and that pressing the button would increment the value by one would look as follows.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../lib/count_widget.dart';
void main() {
testWidgets("CountWidget initial value is 0.", (tester) async {
final myApp = ProviderScope(child: MaterialApp(home: CountWidget()));
await tester.pumpWidget(myApp);
final zeroCountFinder = find.text('0');
expect(zeroCountFinder, findsOneWidget);
});
testWidgets("Value is 1 when the button is pressed once.", (tester) async {
final myApp = ProviderScope(child: MaterialApp(home: CountWidget()));
await tester.pumpWidget(myApp);
await tester.tap(find.text('0'));
await tester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
});
}
Again, when we run the tests, all the tests pass.
flutter test
00:02 +5: All tests passed!
Integration tests?
Flutter provides also the means to create integration tests, which are run on a physical device or an emulator. Such tests allow checking that the application works as expected on specific devices. Integration tests in Flutter follow a similar style to the tests above.
For additional information, see Flutter's documentation on integration testing.