This tutorial is the second part of the Flutter testing tutorial series. In the first part, we learned about writing unit tests, how to write testable code, and how to write a good unit test. If you've missed it, you can read it here.
In this tutorial, you will learn how to test widgets in a flutter. We will cover the following topics
- Writing testable widgets
- Types of widget tests.
If you haven’t already, clone the starter project from here. Once you clone the project, open it in VS Code or Android studio and check out the widget-test-start branch. You will see the following directory structure.
This branch contains all the changes we made in the first tutorial.
We’re going to start where we left off and add widget tests.
What are Widget tests
Widgets are the basic building blocks of a flutter application. Any screen that you interact with has a widget tree.
A widget test is a special type of test that allows us to test widgets in flutter. It takes advantage of the widget tree to run these tests. It creates the tree of widgets without any UI thus saving render time.
Widget tests can be categorised into two categories:
- Widget Tests: These tests deal with small individual widgets that don’t have any dependencies. You can think of them as unit testing for widgets.
- Page / Multi Widget Tests: These tests deal with testing multiple widgets and their interactions with each other. Like testing the behaviour of a page in the application.
Writing testable widgets
To write widget tests you need to first write testable widgets. That means the widgets you write shouldn’t have many dependancies, so that you don’t have many mocks.
Usually, widgets do a lot of things other than just create UI. It can listen to state changes, it can create controllers... the works. Here is an example.
The SearchPageBody widget creates a TextEditingController, it also accesses the state and uses hard coded strings. All of these make the widget hard to test. Here is why:
- For every test you will need to mock the state.
- The TextEditingController is a dependency that is not in your control.
- You cannot manipulate hard coded text.
To make a widget testable, you should separate it into two parts by using the connector pattern. It breaks down a single widget into two, a connector widget and a content widget.
The Connector Widget
This is the widget that will handle all the dependencies. It will listen to state changes, create required classes, and pass all of this to the content widget. Here is what the connecter widget will look like for the SearchPageBody class.
The Content Widget
This widget receives all the data it needs in its constructor and renders the data.
The connector pattern separates the user interface from the rest of the objects. Now you only need to test the content widget. Since everything is passed to this widget you can mock and control all the dependancies.
Widget test setup
This setup section assumes that you have followed the setup steps in part 1. If not, you can go through the first part of this tutorial series here (@Rasha Add link to part 1).
You will write tests for the widgets displayed on the Search page. These widgets are located in the lib/presentation/destinations directory.
First, let’s write tests for the SearchPageResultsContent widget. In the test directory, create a new folder named presentation and since you are writing a widget test, create a widget directory inside.
Next, to make it easier to locate tests for a particular widget, later on, you will replicate the original directory structure. Create the destination/weather/search/widgets directory and create a new file named search_page_results_content_test.dart in it.
The structure of a test file seen in part 1 remains the same for any kind of test. In the newly created test file, add the main method along with empty setUp and tearDown functions for now.
It’s important to know all the different states a widget can be in. This helps in writing better tests as you know beforehand what all you need to test.
The SearchPageResultsContent widget displays a loading indicator when showLoading is set to true and displays a list of data when it is set to false.
Let’s look at the SearchPageResultsContent constructor to figure out if we need to create any mocks.
Here we would need to mock the IntentHandle. In the mocks. dart file, add a new mock class.
Initialize the mock in the test file so that we can use it later when required.
Writing widget tests
To create a widget test, you will use the testWidgets function from the flutter testing library. You will use the same Given, When, Then naming style you used in part 1.
Let’s write a widget test, to test the loading state.
The WidgetTester is a class that allows you to load widgets for testing. It moves the rendering of the widget ahead by a few frames, and lets you interact with them.
Before we load any widget, there are a few things to consider.
- Almost all widgets require a material app and a scaffold to be the parent.
- The specific widget that you are testing, might need some specific parent widgets as well. For E.g. the SearchPageResultsContent widget requires a Flex widget in the widget tree.
You need to account for all these requirements when you load the widget.
Let’s come back to the SearchPageResultsContent widget test. To test the loading indicator you will wrap the widget and pass the needed mocks and parameters as shown below.
Once the widget is loaded, we can interact with it using WidgetTester. We can select widgets to perform actions on or verify conditions using the find function. Let’s check if the loading widget is displayed as expected. To do this, we need to expect one widget of the type SearchPageLoadingShimmer is displayed.
Here you are searching the widget by its type, but there are many more methods to search a widget by, like byText or byIcon.
You can read more about the finders here. findaOneWidget and finds nothing are matchers from the test library that verify that one and zero widgets are found by the given finder.
Run the test. You should see the results as shown below.
Extracting common widget setup tests
Writing the pumpWidget function on each test is not very efficient. The majority of that function will be the same across various test files. Even in a single test file, there are parts of the widget setup that can be extracted to a common function.
Create a new directory in test/presentation named base. Create a file named widget_tester_ext.dart in this directory.
The common widgets that every widget test file will need are MaterialApp and Scaffold. Extract the common widgets to an extension function.
With that in place, each widget test file can have a common loadWidget function that can extract any common widget specific setup. Open the search_page_results_content_test.dart file and add the following function.
Here you will extract the setup of Flex to the common function. Now you can replace the pumpWidget function with our custom helper method in the test.
Interactions in widget test
Widgets react to some sort of user interaction. It might be typing some text, or tapping on an icon. You should be able to mimic the interactions in a widget test as well. To do that you can use the WidgetTester.
Add the following test to the search_page_results_content_test.dart file.
Here you are using the mock intent handler created in the setup process. You pass the onIntent function reference to the intentHandlerCallback parameter of the widget.
Add the code to tap on the favourite icon. You can do that by using the tap method on WidgetTester. You can learn more about all the available methods here.
The pumpAndSettle method advances rendering by multiple frames until nothing is changing on the screen. It this is helpful if there could be some animation involved in changing the UI.
Next, verify that the correct function call was made in response to the tap with the correct data.
Run the test. You should see the results as shown below.
Page / Multi Widget Tests
These tests are similar to widget test, but have a wider scope of testing. Instead of testing individual widgets, here you will be testing a group of widgets or the entire page. Going forward we will refer to these tests as Page tests.
In page tests, you want to mimic real app behaviour as closely as possible. Here you will interact with your UI and expect certain results in response to those interactions.
The two main differences in integration tests from widget tests are:
- State Management mocking setup: Similar to widget tests, you cannot avoid the state management entirely. The page widgets will usually directly deal with some state management logic. Depending on the state management that you use in your application the methods to mock or replace it in tests will differ. Looking into the docs of the state management library for details is a good start.
- The pumpWidget setup: Similar to a widget test. Page tests will also benefit from a custom helper function that extracts all the common pumpWidget logic. For widget tests, the setup was trivial, but for page tests, you need a lot more. Some of these will include your localisation library, global wrapper widget for your state management library, etc. This setup is also very subjective to your application. You need to make sure you provide all the parent widgets required for your page to function correctly.
Apart from these two differences, a page test is not different than a widget test.
Advantages and Disadvantages of Widget Tests
Widget and Page tests are very quick to execute since it does not actually render UI to any screen. You can easily test interactions and their side effects on widgets and pages.
The major disadvantage of these tests is that they cannot safeguard you from UI position changes. For example, if the position of the favourite icon on the list item changes, this test will still pass.
These tests also cannot easily test if your widgets and pages look good across a variety of resolutions. With the number of platforms, flutter can target increasing continuously, this becomes even more important.
We will look at how can we write tests that verify the appearance of the widgets and pages in the upcoming part of this tutorial series.
Where to go from here?
You are now able to test your widgets and pages! Try writing more widgets and integration tests with the concepts you learned. Try to apply them to your projects. To get the complete code from this tutorial, check out the widget-test-end branch.
If you want to look at a working example, check out the widget and integration tests in the flutter template.
In the next part, we will look at tests that can verify the appearance of your UI.
We’d love to hear back from you with your learnings on the topic. Join the conversation over at the article here on Twitter.