Skip to main content

Tests in Flutter - A Deep Tour

When creating our applications it is very important to work on automatic tests that can confirm that a certain behaviour occurs during our development. This will ensure that over time our application continues to behave as expected and the behaviour has not been altered without having to test it manually every time we make changes. We can even have them trigger automatically every time we bring our changes to a certain branch. Welcome to software development engineering.

A group of people designing tests in Flutter and analysing their test coverage.

Within test development there are three types or categories of tests we can perform on our code.

  • Unit test: Tests for a function, method, or class.
  • Widget test: Tests a single widget (in other UI frameworks referred to as Component test or UI test).
  • Integration test: Tests a complete app or a large part of an app.

In order to talk about code testing, we need to talk about the concept of code coverage. Coverage measures the amount of code that is analysed during a test or set of tests. This is very relevant because although we want it to be as high as possible, there will be things that cannot be tested due to their nature and in many occasions we will not reach 100% coverage.


In general, a well-tested application has many unit and widget tests, controlled by code coverage, plus enough integration tests to cover all important use cases. Each of the three categories of testing we have seen has advantages and disadvantages. 

List relating confidence, maintenance cost, dependencies and execution speed to the type of test category in Flutter.

Base code

I was originally going to use Flutter's Codelab for unit testing, but while writing this article I detected several bugs in Codelab due to using the deprecated flutter_driver package. This led me to end up creating my own updated GitHub project for testing based on Google's Codelab. You can download the test project from the following website.

 In this application we will start by building a simple application with a list of items. We provide you with the source code so you can start directly with testing. The application will use the Provider package to manage state, for routing the application will use the go_router package. In addition the application will support the following operations:
  • Adding the items to favorites.
  • Viewing the list of favorites.
  • Removing items from the favorites list.
Application developed in Flutter

The project we will use for learning to test is available in the following repository. We can see the dependencies to go_router and provider that we commented before. 

But we can also see new development dependencies (dev_dependencies) for tests.
  • integration_test: Used for integration testing.
  • flutter_test: Used in Widget testing (UI Testing).
  • test: Used in unit testing.
Build the app to see that everything works perfectly and let's look at this a bit more in depth.

Within the test folder we have 3 folders containing tests organised by each of the types of tests we can carry out mentioned above.

Contents of the test folder of the learning_tests_flutter_app project on GitHub.

Unit Tests - Model Layer

Inside the unit folder we have the unit tests. We can test the Provider layer and the models that our projects have. To do this, for each file in our models folder we will create a test file and for each method we will create a test function. In this example project we have only one model, the Favorites class, so we will only have a single file called favorites_test.dart


Dart code for testing the Favorites model and its provider layer.


Each test file must end with the name test (by naming convention). On the other hand, it must have a main function and inside it it is recommended to group a set of related tests using the group function followed by the name. In our case, "Testing App Provider". 

Within this group, we will test the two functions available in our model. One to test the function of adding elements and one to test the function of removing elements.

You can see the result of the test by executing the command flutter test followed by the path to the test you want to test (in Android Studio we have play buttons all over the tests).

Result of executing the flutter test command in Android Studio

UI Tests - Widgets and App Layer

Inside the widgets folder we have all the UI tests for each widget of our application. To test each of our views we will use the flutter_test package. To do this, for each of our views we will create a file. In our example case, as with the unit tests, we have a folder with the tests for the favourites view, the home view and the whole app. 

Dart code showing how to do UI testing in Dart for a Flutter project.


Thanks to the flutter_test package we can use the testWidgets function where we will render our widget using the pumpWidget function of the tester object captured in the callback. This object that we have in the callback which we have called tester is of type WidgetTester and it will allow us to execute a lot of actions on the widget as for example a very common one is tap(). The tap function requires by parameter the object that we want to press and for it we will have to look for this element in the tree of widgets of our application.

Thanks to the import of the flutter_test library we can make use of a constant called find that the Google team has defined internally to make searches within the tree of widgets that we have available. The types of finds that can be executed are by:

- text: Searches for text (example on line 30).
- byIcon: Searches for an icon (example on line 39).
- byType: Searches for an object type.

The results that can be used as hits most commonly are:

- findsOneWidget: The search result must be 1 and only 1 widget.
- findsWidgets: The search result must be at least 1 widget.
- findsNothing: The search result should not have found any matching widgets.
- findsExactly: The search result must be exactly a specific number of candidates.

By using the pumpAndSettle function essentially waits for all animations to have completed. For example in our case, we use it to make sure that the toast has appeared and we can search for it in the widget tree by text with find.text() method.

Integration Test

Integration tests will allow us to test parts of our code related to operations so that when we modify an operation such as the favourites operation in our application, the rest of the application or screens do not see their result altered.

Dart code showing how to do an integration test in Dart for a Flutter project.


Conclusions

It is very useful to have all this kind of tests within our projects because as changes are made in the code sometimes bugs are introduced that are difficult to locate before going to production. Here is a link to a video where I talk about this in more depth for the Liquid Galaxy community.

Best regards and see you in other posts!


Comments

© 2020 Mobile Dev Hub