Testing In Flutter: Widget Test
What is a Widget test? What are pumpWidget(), pump() & pumpAndSettle()? How to test smaller widgets in Isolation?

Introduction
- In the previous blog, we saw the Unit Test in Flutter.
- Unit Testing is a great testing technique. It allows us to test a single function, method, or class individually. This is a fantastic way to make sure that a unit of logic produces the expected value.
- However, they don't provide much confidence since they focus on small pieces of code and not on how the pieces integrate with each other.
- In this blog, we are going to see another testing technique in Flutter which is Widget Test.
Widget Test
- Widget testing is a technique that allows us to ensure that various portions of the user interface work as intended without the need for a physical device or simulator.
- They can be a great way to isolate small parts of your app and find out whether your code is behaving as expected.
- They usually increase the level of confidence compared to unit tests. And the execution time allows us to run hundreds of tests in less than a minute.
- They will seem familiar to you since they are very similar to the unit test that we have previously written.
- However, due to the
flutter_testpackage, we can utilize several handy tools and utilities to interact with our application. - To continue with our calculator project, we will now add widget testing to our flutter application.
- The first step is to make a
testfolder if you don't already have one. - Next, we'll create our first test file,
calculator_app_test.dart, in which we'll write our test. - Now we are going to create our first tests. This time however we need to make sure that we import the
flutter_testpackage so we can use thetestWidgets()method instead of the previoustest()method that we use for unit test.
import 'package:flutter_test/flutter_test.dart';
void main() {
group(' ', () {
testWidgets(' ',
(WidgetTester tester) async {
}
);
});
}
- To begin testing, we must first develop a page that has some widgets. Let's make a CalculatorApp page that displays four ListTile widgets. Which displays several icons for operations and related titles.
import 'package:flutter/material.dart';
void main() {
runApp(const CalculatorPage());
}
class CalculatorPage extends StatelessWidget {
const CalculatorPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("Calculator"),
),
body: Column(
children: const [
ListTile(
title: Text("Addition"),
leading: Icon(Icons.add),
),
ListTile(
title: Text("Subtraction"),
leading: Icon(Icons.minimize),
),
ListTile(
title: Text("Multiplication"),
leading: Icon(Icons.cancel_outlined),
),
ListTile(
title: Text("Division"),
leading: Icon(Icons.architecture),
),
],
),
),
);
}
}
- To test these widgets, navigate to the
calculator app test.dartfile. - Now create a group called 'CalculatorApp.'
void main() { group('CalculatorApp', () { }); } - Then insert the test description within the
testWidgetsmethod.testWidgetsis liketestin unit testing, but for widget testing.void main() { group('CalculatorApp', () { testWidgets('Render 4 widgets of Type ListTile', (WidgetTester tester) async { }); }); } - The function takes one argument called
WidgetTester, which is used to interact with the application. - When we launch the application, we can see four list tile widgets on the screen. As a result, the most basic test we can write here is: When we launch the application, the four widgets are rendered.
- In a flutter, we use the function
runAppto inflate our application and attach it to the screen of the device. - Similarly the
pumpWidgetmethod from the widget tester class achieves the same. On a test environment it renders the initial UI of the given widget.void main() { group('CalculatorApp', () { testWidgets('Render 4 widgets of Type ListTile', (WidgetTester tester) async { await tester.pumpWidget(const CalculatorPage()); expect(find.byType(ListTile), findsNWidgets(4)); // Expecting 4 ListTile widget on Screen. }); }); }

Difference Between pumpWidget(), pump() & pumpAndSettle()
- Interacting with our application is crucial during the testing phase. After all, being able to tap a button, fill a form, or scroll through views is what adds value to our project.
- Consider the following example, where I’ve created two
Textfieldfor inputting two numbers for calculation. I also gave each of theTextfielda uniqueKeySo that we can find that particular widget inside our tests.

- As you can see as soon as I fill up the Textfields the result gets appeared with a little background color animation.
- Let’s see how to test this behavior in our Calculator Test file.
- First of all, let’s create a group named
add. And then usingpumpWidgetlet’s render theCalculatorPage.
group('add',(){
testWidget('Show result when two inputs are given',(WidgetTester tester) async{
await tester.pumpWidget(const CalculatorPage())
})
})
- Now that we’ve rendered the UI. It’s time to input the numbers in the
TextField. In order to find the particularTextField, I’ve assigned a uniqueKeyto each of them. - In order to enter the value from the test, the
WidgetTesterprovides us a method calledenterText().group('add',(){ testWidget('Show result when two inputs are given',(WidgetTester tester) async{ await tester.pumpWidget(const CalculatorPage()) await tester.enterText(find.byKey(const Key('textfield_top_plus')), '3'); await tester.enterText(find.byKey(const Key('textfield_bottom_plus')), '6'); }) }) - It takes two parameters:
Finder: This must be an EditableText or have an EditableText descendant. For exampleTextFieldorTextFormField, orEditableText.String: It is basically the value which we want to be entered in the Field.
- Once the text is typed into the textfield, the result is shown, as seen in the output above.
- So now we can check if there is a text widget on the screen that has the string
Result: 9or not. - To do this, we can use the
find.text()function to obtain the Text, and then usefindsOneWidgetto determine whether or not any widget has the expected string.
group('add', () {
testWidgets('Show result when two inputs are given',
(WidgetTester tester) async {
await tester.pumpWidget(const CalculatorPage());
await tester.enterText(find.byKey(const Key('textfield_top_plus')), '3');
await tester.enterText(find.byKey(const Key('textfield_bottom_plus')), '6');
expect(find.text('Result: 9.0'), findsOneWidget);
});
});
- If you run the test, you will get an error that says
zero widgets with the text "Result: 9.0". This means none were found but one was expected. Why did this happen? - This is due to the fact that when we input a number, the UI changes and shows the result. This change in the UI, or the new frame that was rendered, is exactly what we are lacking in our test.
- We need to figure out how to inform our widget test to render a new frame after we enter both numbers.
- The answer is to use the tester instance to invoke the
pump()method. pump()instructs the system to paint a new frame so that we can meet our expectations with a newly updated user interface.
group('add', () {
testWidgets('Show result when two inputs are given',
(WidgetTester tester) async {
await tester.pumpWidget(const CalculatorPage());
await tester.enterText(find.byKey(const Key('textfield_top_plus')), '3');
await tester.enterText(
find.byKey(const Key('textfield_bottom_plus')), '6');
await tester.pump();
expect(find.text('Result: 9.0'), findsOneWidget);
});
});
- However, the method
pump()is rather restricted. Because it just refreshes a single frame, which is useless when working with animations. - For instance, in our Calculator Page, let's show the result only when the background animation has finished.
- To do this, I've created a new variable called
resultAfterAnimation, and I'm setting its value when the animation has finished.
AnimatedContainer(
padding: const EdgeInsets.all(8),
duration: const Duration(milliseconds: 1000),
onEnd: () {
setState(() {
resultAfterAnimation = result.toString();
});
},
color: result == null ? Colors.transparent : Colors.green,
curve: Curves.easeInOut,
child: Text(
resultAfterAnimation != null
? 'Result: $resultAfterAnimation'
: 'Result: ',
style: Theme.of(context).textTheme.bodyText1,
textAlign: TextAlign.end,
),
),
- If you try to run the test now, you will receive an error. It's because there is no longer a single frame. The animation is updating many frames at the same time.
- To wait for the animation to finish, we must use the
pumpAndSettle()function provided by theWidgetTest. - Simply replace the
pump()method withpumpAndSettle()and you're done.
group('add', () {
testWidgets('Show result when two inputs are given',
(WidgetTester tester) async {
await tester.pumpWidget(const CalculatorPage());
await tester.enterText(find.byKey(const Key('textfield_top_plus')), '3');
await tester.enterText(
find.byKey(const Key('textfield_bottom_plus')), '6');
await tester.pumpAndSettle();
expect(find.text('Result: 9.0'), findsOneWidget);
});
});
Finding a Widget in Widget Tree Using CommonFinders
- Finding and locating the widget is very important in the Widget Test.
- The flutter test Package provides top-level find methods that allow us to locate the widgets.
- While there are plenty of ways to find the widgets, the most common method is finding widgets using
byKey(). byType(): If we know the class name of the widget that we want to locate orbyText(): If what we want to locate is a certain string on the screen- You can find more common finders by visiting this link
Interacting with Your Widgets Using WidgetTester
- Interacting with the Widget is very important in Widget Test. After all what we want to validate in most cases is that after the user interacted with our app like a button tap, our application transitions to a known state that we can assert in our tests.
- The
testWidget()method provides us a callback that gives ustesterinstance of aWidgetTester. - With this object, we can interact with the widget that we have previously inflated with the
pumpWidget(). (In our case that isCalculatorPage()) . - In our test, we can use
testerthatensureVisible()to make sure that a given widget is visible within a scrollable view. - Then we can use
tester.tap()on TextFormField so they can gain focus. And
tester.enterText()to type the given text in the text field .group('add', () { testWidgets('Show result when two inputs are given', (WidgetTester tester) async { await tester.pumpWidget(const CalculatorPage()); final topTextFieldFinder = find.byKey(const Key('textfield_top_plus')); final bottomTextFieldFinder = find.byKey(const Key('textfield_bottom_plus')); await tester.ensureVisible(topTextFieldFinder); await tester.tap(topTextFieldFinder); await tester.enterText(topTextFieldFinder, '3'); await tester.ensureVisible(bottomTextFieldFinder); await tester.tap(bottomTextFieldFinder); await tester.enterText(bottomTextFieldFinder, '6'); await tester.pumpAndSettle(); expect(find.text('Result: 9.0'), findsOneWidget); }); });- You can study plenty of other tester methods here
Testing Smaller Widgets in Isolation
- One of the greatest benefits of widget testing is that we can test different widgets and components in isolation without needing to launch the application.
- By doing this, our tests can be much more focused, faster and our code base will be much more modular, developer-friendly, and easy to maintain over time.
- Continuing with our calculator project we are going to apply this principle to the
OperationWidget.

- To test this widget in isolation the approach did not differ from what we've already learned.
- The first step will be to pump our widget under test using
pumpWidget(). - Our
OperationWidgethas five dependencies in its constructor, the icon, title, two keys, and the operation enum. - Now we will pump our
OperationWidgetusingpumpWidgetand all the other things remain the same.
main() {
group('TwoDigit Addition Operation', () {
testWidgets('render 10 when 5 and 5 added', (tester) async {
final topTextFieldFinder = find.byKey(const Key('textfield_top_plus'));
final bottomTextFieldFinder =
find.byKey(const Key('textfield_bottom_plus'));
await tester.pumpWidget(
OperationWidget(
operationIcon: Icons.add,
operationTitle: "Addition",
operationType: OperationType.add,
textFieldTopKey: 'textfield_top_plus',
textFieldBottomKey: 'textfield_bottom_plus',
),
);
await tester.enterText(topTextFieldFinder, '5');
await tester.enterText(bottomTextFieldFinder, '5');
await tester.pumpAndSettle();
expect(find.text('Result: 10.0'), findsOneWidget);
});
});
}
- But now if you run this test, you will get an error saying: Saying
The specific widget that could not find a Material ancestor was: TextField. - In order to solve this issue, we need to wrap the
OperationWidgetaroundMaterialAppandScaffoldwidget.
main() {
group('TwoDigit Addition Operation', () {
//...
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: OperationWidget(
operationIcon: Icons.add,
operationTitle: "Addition",
operationType: OperationType.add,
textFieldTopKey: 'textfield_top_plus',
textFieldBottomKey: 'textfield_bottom_plus',
),
),
),
);
//...
});
});
}
- Now if you will run the test, it should pass.
Wrapping Up
- I hope you enjoyed and learned something from this article. If you have any feedback/queries, leave them in the comments.
- In the next blog we are going to see the final testing method: Integration Test
- I've learned to test and wrote this article using the example provided by VGV. So big thanks to VGV.
- Thank you for spending time reading this article. See you in the next article. Until then...






