Flutter widget tests: a practical example

When it comes to mobile apps, automated tests play a very important role in app maintenance. So, how do you write tests in Flutter?

As you would expect from a modern framework, Flutter offers both unit and integration tests. But it also offers something else: widget tests.

Widget tests are in between unit and integration. Like a unit test, they run on a simpler test environment (whereas integration tests run on an emulator or device). But they can actually test the UI. You can write a widget test that taps on a button.

With the Android SDK, I write UI tests with Espresso. It works well but the tests take a long time to run. It’s not a problem with CI, as we can use some good sharding libraries. But while I develop, I often run a subset on my local machine, to verify my work, and I get frustrated.

Therefore, I find the concept of widget tests very welcome. It will reduce the number of integration tests on a production app, moving some of them to widget tests.

In this code tutorial, we will create an app with a list page and a details page. On the details page, we can select/unselect an item. Its selected status is shown on the list page via a different background colour. 

Then,  we will write unit and widget tests to verify the behaviour of the app.

widget tests
widget tests

The full source code is available on Github.

General approach

We will use a stream to hold the list of items.

We will load the data from a json file, and not from a server. It keeps this code tutorial free-standing (ie not relying on a server). It doesn’t change anything about the testing, it only changes the implementation of the method that gets the data.

We will add a mechanism to inject test data in the widget test. So we can write a widget test simulating there was an error loading the data for example. To do this, we will introduce the concept of “DataProvider”, which the bloc will use. The bloc will provide a method to pass in a different one.

Setting up the app

To follow the code tutorial, create a new app as follows.

If you’re unsure how to set up a Flutter app, check out Getting started with Flutter official tutorial.

There are 2 main widgets, ListPage, and DetailsPage. ListPage is the entry point of the app.

First, we amend main.dart.

Secondly, we create list_page.dart in a new list directory.

Thirdly, we create details_page.dart in a new details directory. We will open that screen using the id of the item shown on ListPage, so we pass that in when building that widget.

Fourthly, we create strings.dart and add all the strings to it. As we fill in the UI, how those strings are used will become clear 😉

Lastly, we add rxdart library to pubspec.yaml and run flutter get packages .

Loading and displaying the list data

Data source

For simplicity, the data is loaded from a json file. Let’s create a top level assets folder, and a data.json file in it. Copy the contents from here.

We also need to include the new asset file in pubspec.yaml.

Business Logic Component (BLoC)

For the UI/model architecture, we will use a bloc implemented as a singleton. For more complex apps, I recommend using a BlocProvider as described in Reactive Programming – Streams – BLoC. Mostly, I recommend it, because it handles closing the stream as part of the widget lifecycle. But in our case, we will use a singleton for simplicity, and call its method to call the stream on the dispose method of MyApp.

Let’s create list_bloc.dart, in list directory.

We need to set up the model now. We will use Item for an item, and ListOfItem, for the list, which can include either an error message or an actual list of Items. We create a file model.dart for them, in list directory. Item will be loaded from a json file, so let’s write that in.

The bloc is  a stream of ListofItems. So let’s set that up, not forgetting a method to dispose of the stream, which we call from MyApp, in main.dart.

Now, we need to load the data from the json file. To make it testable, we want to abstract the idea of “data provider”. So we create an abstract class, and set up our bloc to use it.

You’ve noticed BackendData above? That’s our implementation to get the data in the app. Add a backend directory, and create backend_data.dart in it. For a reminder on parsing Json, read How to parse JSON in Dart/Flutter.

Let’s amend list_bloc.dart to load the data.

UI

And finally, we can build ListPage to use the stream. Depending on the state of the stream data, we either display a CircularProgressIndicator, an error message, or a list of items. When tapping on an item, the app goes to DetailsPage. If an item is selected, the background colour is green.

Displaying the details page

Similar to the setup for ListBloc, we set up a singleton DetailsBloc. Due to the UI of the app, there is only one DetailsPage at any time. Therefore, we can use a singleton Bloc. If there could be several instances of DetailsPage in the widget tree at a given time, we would need to add the item id as a parameter to the Bloc class. DetailsBloc uses a stream of Item.

DetailsPage knows the id of the item, so let’s implement a method to get the Item from the Bloc, by id.

We have everything in place to amend DetailsPage now. The app bar shows the name of the item. Below it, the title is shown, along with ” – Selected” if selected. Below that, a description is shown. Finally, there will be a button to select/unselect an item, which we will implement in the next section.

And let’s not forget to dispose of our bloc when the app is finished, in main.dart.

Handling the user interactions on details page

The two actions are selecting and deselecting an item. This is done on DetailsPage, by tapping a button.

We have setup DetailsBloc to listen to the stream from ListBloc, and update its own stream when there is an update to the current item. Therefore, to select/deselect an item, all we need is to handle select/deselect in ListBloc() – the result will be reflected in ListPage and in DetailsPage.

We can now configure a button in DetailsPage to actually enable that functionality.

Unit testing the blocs

ListBloc contains quite a bit of logic. It loads data from an ItemsDataProvider, sort the entries, selects, and deselects items. So we can write a few unit tests for it.

In test folder, remove the default test file created with the project (widget_test.dart). We start with setting up data providers for tests. Create a backend folder, with test_data_provider.dart and test_data_provider_error.dart, as below.

Let’s write our unit tests for ListBloc now. We create a new folder, list, then list_bloc_test.dart as below.

Our first step loads data and verifies the correct data is added to the stream. To verify that, we use the “take” method and convert it to a list. We can pass a number to take, so we can take more than the last event of the stream (we’ll use that in other tests). The rest of the test is simple: create ListBloc instance, inject test data provider, load items, take last event from stream, verify that event contains the data we expect.

We can now write some tests around selecting and deselecting items. For those, we take the last 2 events of the stream, ie the ListOfItems before the selection change, and the one after the selection change.

Testing ListPage

Now, we want some UI tests, but we would like them to run fast. Widget tests to the rescue!!!

Setting up a widget test is quite simple, though a little bit of boilerplate code is required. We wrap the widget under test inside MaterialApp, and “pump” that widget. Let’s look at an example, checking the items are displayed, by creating a new file list_page_test.dart, under list folder.

The set up we follow is: set up ListBloc with test data provider, create and launch ListPageWrapper, pump the widget again (more on that later), define some items to find by text and find them, define some predicates for more complicated way to find items (ie Container background colour) and find them.

As with most UI tests, it’s not trivial to check the order of an item in a list but we have tested that with the ListBloc unit test. What we do here is  simply using 2 unselected and 1 selected items, so we can verify the background colour of the selected widget by number.

What other widget tests could we write? Error message, and that the widget is updated when the stream is updated. The first one is simple, a copy of the test above but with a simple error message verification. The second one is a bit more complicated, but it’s basically doing the same thing twice, not forgetting to change the data provider for the second time (so the data effectively changes).

At this point, you may ask yourself this question: What is this widget pumping thing?

tester.pump

This is basically a way to trigger a frame redraw for the widget. That’s a bit weird at first, but I got used to it quickly. The test expects the UI should be redrawn, so it says “let’s see what it looks like when we redraw it”.

Testing DetailsPage

Now, we can write tests for DetailsPage, related to the selected/unselected state. This is when widget tests get very interesting, because you can do some UI actions, such as tap on a button.

First, we’ll write tests to confirm a selected item is shown as selected, and its mirror test, an unselected item is shown as unselected. Let’s create details/details_page_test.dart.

Now, we can write tests that select an unselected item, and vice versa. Note that, as explained above, those tests use test.pump after tapping on the button.

What next?

In this code tutorial (source code), we were able to write tests that verify not only the logic but also the UI interaction of our app. And those tests took just under 1 second to run (on my machine). That’s great news!

There is, of course, still a place for integration tests, so I encourage you to try them out. But having a category of tests in between unit and integration tests is simply awesome 🙂

Author: Natalie Masse Hooper

Mobile app developer with 14 years experience. Started with the Android SDK in 2010 and switched to Flutter in 2017. Past apps range from start ups to large tech (Google) and non tech (British Airways) companies, in various sectors (transport, commercial operations, e-commerce).

Leave a Reply

Your email address will not be published.