A Deep Dive Into Widget Testing in Flutter: Part II (Finder and WidgetTester)

Understanding the Ins and Outs of Widget Testing in Flutter

Deven Joshi
Flutter Community
8 min readApr 17, 2021

--

This article continues Part I of Widget Testing in Flutter.

Welcome back to the Widget Testing Deep Dive 👋

Last time, we primarily explored the basic test file structure as well as a deeper look into what the testWidgets() function can do in a test. While we’ve gone through that the function runs a test, we haven’t actually run a test , or even seen what it looks like — and that’s on purpose. It is my belief that knowing the components that make up a test well first can give us a tremendous boost when we actually start writing them.

Here is a small summary from last time

  1. Widget tests test out smaller components in an app
  2. We write our tests in the test folder
  3. The testWidgets() function is the function in which we write our widget tests — and we saw the function broken down in detail.

Let’s continue.

How Is A Widget Test Written?

A Widget Test is usually written for one of two things:

  1. Checking if visual elements are present
  2. Checking if interaction with visual elements gives a correct result

Let’s go with the second type since the first type can be added in as well. To do this, we usually follow a few steps in the test:

  1. Set up requisites and create (pump) a widget to test with
  2. Find the visual elements on the screen via some kind of property (such as a key)
  3. Interact with the elements (such as a button) using the same identifier
  4. Verify that the results match what was expected

Creating (Pumping) A Widget To Test

To test a widget, we need a widget — duh. Let’s look at the default test in the test folder:

You may have noticed the WidgetTester object provided to us in the callback where we write our test. It’s time to put that to use.

To pump a new widget to test, we use the pumpWidget() method.

(Don’t forget the await or else the test will complain beyond belief)

This inflates a widget for us to test with.

We will go into more detail about the WidgetTester in just a bit, but there’s something else we need to know better first.

Understanding Finders

I have to admit, I had a jamais vu moment with the word ‘finding’ in the course of writing this article and it’s still a bit annoying in my head.

While the first step involves instantiating a widget for testing, the second step on our quest to write a test involves finding a visual element we want to interact with — might be a button, text, etc.

So how do we find a widget? We use a finder. (You can find elements as well, but I digress.)

That’s easy enough to say but in reality, you need to identify something that’s unique to the widget — the type, the text, the descendants or ancestors of it, etc.

Let’s look at some common and some of the more interesting ways to find a widget:

find.byType()

Let’s take an example of finding a Text widget:

Here, we use a predefined instance of the CommonFinders class called ‘find’ to create a finder. The byType() function helps us identify ANY widget of a particular type. So if two texts existed in the widget tree, BOTH would be identified. So if you intend to find a particular Text widget, consider adding a key or doing the next type:

find.text()

To find a specific Text widget, use find.text():

This also works for any EditableText such as a TextField widget.

find.byKey()

One of the most common ways and easiest ways to find a widget is just by attaching a key to it:

find.descendant() and find.ancestor()

This is a more interesting type where you can find a descendant or ancestor of a widget matching some particular property (which we again use a finder for).

Say we want to find an icon that is a descendant of a Center widget that has a key, we can do:

We specify here that the widget we intend to find is a descendant ‘of’ the Center widget and matches properties that we identify with a finder again.

The find.ancestor() call is mostly similar but the roles are reversed since we are trying to find a widget above the widget identified by the ‘of’ parameter.

If here we were trying to identify the center widget, we would do:

Creating A Custom Finder

What we are doing when we use find.xxxx() is using a Finder that is predefined. What if we want our own way to find a widget?

Continuing our train of bad examples, let’s say we want a Finder that identifies all icons with no keys, let’s call the finder a BadlyWrittenWidgetFinder.

  1. We first extend the MatchFinder class

2. The matches() function is where we check if the widget meets our conditions. In our case, we need to check if the widget is an icon and if the key is null:

3. Using the convenience of extensions, we can add this finder directly into the CommonFinders class (The class which ‘find’ is an instance of):

4. Due to extensions, we can call the finder like we would call any other:

Now that we know a bit more about finders, let’s go on to the WidgetTester.

Everything to know about the WidgetTester

This is a large enough topic to be a different article but we’ll try to cover most of it.

The WidgetTester allows us to interact with the test environment. Widget tests don’t exactly run how they would run on a device since the asynchronous behavior in a test is mocked rather than real. There are also other differences to note:

setState() does not work as usual in a widget test.

While setState() marks the widget to be rebuilt, it does not actually rebuild the tree in a widget test. So how do we do it? Let’s look at the pump methods:

Understanding The Pumps

TL;DR: pump() triggers a new frame (rebuilds the widget), pumpWidget() sets the root widget and then triggers a new frame, pumpAndSettle() calls pump() until the widget does not request new frames anymore (usually when animations are running).

A Bit About pumpWidget()

As we saw before, we used pumpWidget() was used to set the root widget for testing. It calls runApp() using the widget provided and calls pump() internally. If the function is called again, it rebuilds the entire tree.

A Lot About pump()

We need to call pump() to actually rebuild widgets that need to be rebuilt. Let’s say we have a basic counter widget like this:

The widget simply stores a count and updates it when a FloatingActionButton is hit, like the default counter app.

Let’s try testing the widget by finding the add icon and pressing it to verify if the count turns to ‘1’:

…not quite

The reason is, we rebuild the Text widget that displays the count using setState() in the widget, but that does not rebuild the widget. In addition, we also need to call the pump() method:

Which has a much more pleasing result:

If you need to schedule a frame after a specific duration, pump() also takes a duration, which schedules a rebuild AFTER that duration:

Note that the test does not actually wait for that duration, rather, the clock is pushed ahead by that duration.

Here’s a cool thing about the pump: you can actually stop it at a particular phase of being built and rendered. To do this, we set the EnginePhase parameter of the method:

Note: I have included the enum from the source for better understanding the phases, do not add this to the code

Going To pumpAndSettle()

The pumpAndSettle() method is basically the pump method, but called until no new frames are scheduled. This helps finish all animations.

It has similar duration and engine phase parameters but also adds a timeout parameter for capping how long it can be called.

Interaction With The Environment

The WidgetTester allows us to add complex interactions beyond the usual finder + tap interactions as well. Here are a few things that you can do:

tester.drag() allows the creation of a drag from the middle of a widget that we identify with a finder by a certain offset. We can specify the direction of the drag by specifying the x and y slopes of the direction:

We can also create a timed drag using tester.timedDrag():

If you don’t want to use finders and instead just want to drag from one position on the screen to another, use tester.dragFrom() which allows you to start the drag from a position on the screen.

Similarly, a tester.timedDragFrom() exists as well.

Note: If you want to recreate flings, use tester.fling() instead of tester.drag()

Creating Custom Gestures

Let’s try creating our own gesture which will tap a position on the screen and create a box-like gesture from the position to itself.

First, we need to initialize the gesture:

The first parameter defines where the initial down gesture occurs.

Then, we can use this to create our own custom gesture:

There are also other things in the tester like getting the positions of the widgets involved and interacting with the keyboard and such — however they are mostly trivial and maybe I’ll cover them in a later article. This probably might also have something to do with it being 5 AM as I write this, we never know.

In this part, we covered the ins and outs of Finder and WidgetTester. Next up, we complete the testing journey and explore more variants of testing, coming up in Part III.

That’s it for this article! I hope you enjoyed it, and be sure to follow me for more Flutter articles and comment for any feedback you might have about this article.

Feel free to check out my other profiles and articles as well:

--

--

Deven Joshi
Flutter Community

Google Developer Expert, Flutter | Technical Writer | Speaker