Testing in Flutter – Complete Guide

Featured image for Testing in Flutter.

You might be very great at creating applications using Flutter, but unless you know how to automate your tests you might be hampering your productivity. As a beginner in testing, you might wonder about the need for testing in Flutter and why you’d want to test something when you can run the application and test it for yourself.

We will proceed slowly while clearing concepts, we will also create a demo app to get a clear idea as to how to implement testing. Firstly, Let’s take a look at why would we need Testing in Flutter?

Introduction to Testing 🧠

Testing as the name suggests refers to testing something. That something could be your logic, your widgets, your API call, etc. While you can do all this testing by yourself there are some issues you would face in this route:

  • The time involved in running a complete application, just to check a feature.
  • As the app grows larger, we might miss testing some features.
  • While scaling the app to check if the pre-existing code is working as expected is a long and tiresome task.
  • The cost involved to hire someone to test your app and the time involved to do the same. (Ps: Given that he doesn’t miss something 😏)
  • Human operates slower and is bound to commit mistakes.

Due to all these issues mentioned and many others, it’s a must to automate testing. From a function to a feature we can test every type of code that you would write in an application. Now that we have our reason let’s dive into Testing in Flutter.

Types of Testing in Flutter πŸŒ€

Testing in Flutter is categorized into three categories according to the time and complexity involved. We will not bother with implementation in this stage we will look at the theory in this part and discuss the implementation part later. The three categories of testing are:-

1. Unit Testing

This type of testing is used to test a unit of code. Be it a function, or a class in your source code. It is a small piece of code that is run in isolation. We may use Mock API with fake data to check the output of the testing code.

2. Widget Testing

In widget testing, we check if a widget behaves as expected when some value is passed. The value can be mocked or real-world data can be passed to the widget.

3. Integration Testing

This testing involves the testing of complete application in real-world conditions. Here, real API data is used and the complete application flow is checked and tested.


Now that we know the types of testing, let’s compare them on some parameters:

UnitWidgetIntegration
Execution SpeedFastestFastSlow
DependenciesFewMoreMost
Frequency of UseHighLowModerate

As you can notice that every type of testing has some drawbacks and some advantages. Normally, I wouldn’t suggest you use widget testing much. Write a lot of Unit tests and a few Widget tests. There would normally be one or two integration tests showing the complete app flow.

Testing in Flutter. Unit, Widget and integration testing.
The Triangle of testing

What we will build in this article?

We will get an image fetching application that will use Pexels API to get images. The app will have two pages, one page will show a list of random images while another page will be a page to search for images. We will create tests along with the app as we proceed. For app reference, this is a video showing the working of the final application.

To use the Pexels API you will need a Authorization get your auth key now. You may also use any Image API of your choice.

Setting Up the Project

As discussed earlier, Unit tests are used to check small blocks of code. In our application, we would need two APIs one to get the list of random images and another to search for a list of images. So first let’s create an API helper class that will call the APIs for us and return data.

// # lib/data/api_helper.dart

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';

import 'package:http/http.dart' as http;

import '../models/pexels_image.dart';
import '../models/pexels_search_result.dart';

//TODO: Place your authKey here
const String authKey = "placeKeyHere";

const String imageSearchUrl = "https://api.pexels.com/v1/search?query=";
const String curatedImageUrl = "https://api.pexels.com/v1/curated?per_page=20";


// 1
StreamController<PexelsSearchResult> searchStream =
    StreamController.broadcast();

// 2
Future<void> getImageFromQuery(String query) async {
  debugPrint("Getting Image From Query $query");
  http.Response response = await http.get(
    Uri.parse(imageSearchUrl + query + "&per_page=20"),
    headers: {'Authorization': authKey},
  );

  PexelsSearchResult result = pexelsSearchResultFromJson(response.body);
  searchStream.sink.add(result);
  debugPrint(result.toString());
}


// 3
Future<List<PexelsImage>> getCuratedImages() async {
  debugPrint("Getting Curated Image");
  http.Response response = await http.get(
    Uri.parse(curatedImageUrl),
    headers: {'Authorization': authKey},
  );
  List list = json.decode(response.body)['photos'] as List;
  List<PexelsImage> imageList = list.map((e) {
    return PexelsImage.fromJson(e);
  }).toList();

  debugPrint(imageList.toString());
  return imageList;
}

Step by step explanation:

  1. Declaring a StreamController to update the SearchPage about the image query. If you don’t know about Streams you can check my post on Streams in Flutter.
  2. Function to search a query and add the results to the searchStream.
  3. Function to return a Future of random images.

Ensure to place the Pexels Auth Key before continuing otherwise the API will not work πŸ€·β€β™‚οΈ

Now let’s prepare the models we will need, we will not discuss how to create models. Place the models inside the lib/models folder. You can copy the two models prepared below or download them from the source code.

Now normally, after doing this all work, what you would do is create the UI. But remember we were going to test without any need for User Interface because we are unit testing, we can test the smallest block of code, here we will test the functions we created.

Unit Testing in Flutter ✍

For Unit testing, we write the tests inside the test folder. Flutter by default provides us a widget_test.dart file inside the test folder. We will write our unit and widget tests here. However, you may create new files and write separated tests for each page. In our example for sake of simplicity, we will write all our unit tests and widget tests here.

Directory for tests in Flutter. Unit, Widget and Integration Tests.
Tests Directory in Flutter

In simple terms a unit test would look like this:

// # test/widget_test.dart

void main(){

test("Define what the test will do", (){

   // Arrange: Get the resources needed for testing

   // Act: Perform operations you need

   // Assert: Check if the output generated is the one you expected

});

}

We mainly write a test in three phases arrange, act, and assert. You can read more about these phases and the need here. Enough theory, let’s implement our first test ✨

Test #1

// # test/widget_test.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:tests_flutter/models/pexels_image.dart';

import 'package:tests_flutter/data/api_helper.dart';


void main() {
// Place multiple tests here inside the main method

  test("Check if the RandomImages API and JSON to model conversion is working fine",       
    () async {
         List<PexelsImage> curatedImages;
         curatedImages = await getCuratedImages();
         expect(curatedImages, isNotNull);
      });
}

We can run the test using the command-line interface (CLI) or using the run button that appears above every test you write. The command-line command is:

flutter test test/widget_test.dart

When we try to run the following #1 test we will get the following error:

Test failed. See exception logs above. The test description was: Should test widget with http call

Warning: At least one test in this suite creates an HttpClient. When running a test suite that uses TestWidgetsFlutterBinding, all HTTP requests will return status code 400, and no network request will actually be made. Any test expecting a real network connection and status code will fail.
To test code that needs an HttpClient, provide your own HttpClient implementation to the code under test, so that your test can consistently provide a testable response to the code under test.

In simple terms, the error states that we can not use real-world APIs in the tests. That is what was predefined as we were supposed to mock API in unit and widget testing. If you want to mock your API you can check the following post, which explains how to mock API using Mockito.

In our case, we want to test our logic using the Real API so we will need to create our own HttpClient as the error suggests. We just need to simply add the following block of code to create our own HttpClient:

import 'dart:io';

class _MyHttpOverrides extends HttpOverrides {}

To use the custom HttpClient just add the following line to the test:

test("Check if the RandomImages API and JSON to model conversion is working fine",       
    () async {
        // Add this line of code
        HttpOverrides.global = _MyHttpOverrides();

        List<PexelsImage> curatedImages;
        curatedImages = await getCuratedImages();
        expect(curatedImages, isNotNull);
});

Now, the test will execute successfully and we will receive a green tick to the right of our test. This means that the test was executed successfully. So our first test has now been executed successfully now we will write another test to check the search images function.

Test #2

test("When given Trees as query to the Search Image function check if any output is printed", () async {
 // Because we are using the real Image API we need to place custom HttpClient
 HttpOverrides.global = _MyHttpOverrides();

 await getImageFromQuery("Trees");
});

We don’t assert anything in this test, however, we print some data inside of the getImageFromQuery function. So we should see some output on the debug console.

Getting Image From Query trees
PexelsSearchResult(totalResults: 8000, photos:[...], perPage: 20)
βœ“ When given Trees as query to the Search Image function check if any output is printed

Thus, we can say that we did receive a response and we were able to convert it to PexelsSearchResult Model without any error.

Now a point to note is that the code to use custom HttpClient in every test is repeating, to avoid the repetition we can simply add the common code to the setUpAll function. The setUpAll function executes before any test:

import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:tests_flutter/data/api_helper.dart';
import 'package:tests_flutter/models/pexels_image.dart';

void main() {
//  The setUpAll function sets up objects before running any test
  setUpAll(() async {
    HttpOverrides.global = _MyHttpOverrides();
  });

test("Check if the RandomImages API and JSON to model conversion is working fine",       
    () async {
        List<PexelsImage> curatedImages;
        curatedImages = await getCuratedImages();
        expect(curatedImages, isNotNull);
  });

test("When given Trees as query to the Search Image function check if any output is printed", 
    () async {
         await getImageFromQuery("Trees");
  });

}

Now that we have reduced the repetitive code let’s move on to the next test.

Test #3

Before writing test #3 let’s create a method to add custom delay, we will use this method several times ahead in this unit test and widget tests as well.

addDelay(int millis) async {
  await Future.delayed(Duration(milliseconds: millis));
}

In this test, we will check if the StreamController we defined earlier in lib/data/api_helper.dart works fine when a query is made:

test(
"When trees images are searched check if values are received in the stream",
      () async {
    StreamSubscription subs = searchStream.stream.listen(null);
    subs.onData((data) {
      debugPrint("Received Data");
      expect(data, isNotNull);
      subs.cancel();
    });
    await getImageFromQuery("trees");
    // Waiting for the Stream to take up event
    await addDelay(2000);
  });

In this test, firstly we listen to the searchStream and save the StreamSubscription so that we can cancel the subscription later. We manually wait for 2 seconds so that we can receive the stream event as the response from Pexels API may take some time. When we receive the data we assert that the event added is not null.

On running the test we receive a green tick representing that we received non-null data in the stream. We receive the following output on the console:

Getting Image From Query trees
PexelsSearchResult(totalResults: 8000, photos:[...], perPage: 20)
Received Data
βœ“ When trees images are searched check if values are received in the stream

We can also group multiple tests into a group so that we can run them together. To create a group we use the group function and provide the tests within it:

group("Group Name", (){

// test 1
// test 2
// test 3   

});

Now we have written all the unit tests, now we will write some widget tests to ensure that the widgets are also working as expected.

Widget Testing in Flutter πŸ§ͺ

At this point, we are certain that our functions are working as expected now we need to see if the UI is also working as expected with our tested and full-proof backend. 😎

Widget Testing in Flutter is all about testing the widgets, so will need some widgets to test. However, we will not discuss how to create the layout here as we are concerned about testing here. To simply put it the Home page will use FutureBuilder to consume the getCuratedImages function and the Search Page will use StreamBuilder to consume the searchStream defined in api_helper.dart. There will be a simple page to view the image on fullscreen.

You can copy the code from the pages presented below or download the source code.

Place the pages inside the lib/pages directory, now we can test the widgets easily. We can place the widget tests inside test/widget_test.dart or create a separate file. For the sake of simplicity, we will place the widgets test just below our unit tests.

Note that while creating a separate file for test ensure the syntax matches to filename_test.dart. The _test marks the file as a testing file.

To write widget tests we follow these steps:

  1. Firstly we use tester.pumpWidget() to pump the widget.
  2. Then, we use tester.pumpAndSettle() which is a kind of setState but for the tester side. It repeatedly calls pump for a given duration until there are no frames to settle, which is usually required when you have some animations or navigation.
  3. Now we can easily assert our cases. We assert the cases using expect(actual, matcher). You can check the list of all the matchers here.

Test #1

testWidgets(
      "When given trees as query check if the images are loaded",
      (tester) async {
      // 1
    await tester.runAsync(() async {
     
      await tester.pumpWidget(const MaterialApp(home: SearchPage()));
      await tester.pumpAndSettle();
      
      // 2
      await tester.enterText(find.byType(TextField), "Trees");
      // 3
      await tester.tap(find.byType(IconButton));

      // 4
      await addDelay(2000);
      await tester.pump();

      // 5
      expect(find.byType(Image), findsWidgets);
    });
  });

Code Step by Step explanation:

  1. We use the runAsync function because Flutter Widget Testing doesn’t support real asynchronous code here. So if we want to use our addDelay function we need to use the runAsync function.
  2. Using enterText() we enter the text in a text field. We can find the object using find.byType you can check other types of finders here.
  3. Using the tester.tap we press any item and pass the finder object as a parameter.
  4. We add a delay so that we can get the response from the API in the meanwhile, then we call pump() which acts like setState.
  5. Lastly, we assert our condition here. We here are expecting many images because we will be displaying a grid of images.

With that our widget test is complete and now we can run our test. On running we can test we can see that our test passes and we get the following output on the console:

Connection State: ConnectionState.waiting
Getting Image For Query Trees
PexelsSearchResult(totalResults: 8000, photos:[...], perPage: 20)
Connection State: ConnectionState.active
βœ“ When given trees as query check if the images are loaded

Here, the text highlighted in grey is the output printed by the front end in FutureBuilder’s builder method. Also, we get our message that the images are successfully loaded, as we were able to find multiple images. In this application, we will only create one widget test as there is not much complexity to test.

Integration Testing in Flutter 🧩

To write integration tests in Flutter, we need to add a dependency and create a custom file to write integration tests. So let’s add the dependency first, in pubspec.yaml add the following dependency under dev_dependencies:

dev_dependencies:
  flutter_test:
    sdk: flutter
  # Add The following two lines                   
  integration_test:
    sdk: flutter

Save the file and run flutter pub get in the terminal:

> flutter pub get

Now let’s create the directory where we will place our integration tests. Create a folder integration_test in the root directory of your project and create a file app_test.dart. Place the following basic code within the file:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'package:tests_flutter/pages/home_page.dart';
import 'package:tests_flutter/pages/search_page.dart';

void main() {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
      as IntegrationTestWidgetsFlutterBinding;

// fullyLive ensures that every frame is shown, suitable for heavy animations and navigation
  if (binding is LiveTestWidgetsFlutterBinding) {
    binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
  }

// TODO:  Place your integration tests here
}

Integration tests are used to check the complete app or to check the flow of the application. So here real HTTP requests are made and run in a real async environment. So we don’t need to create our custom HttpClient or use tester.runAsync to run asynchronous statements.

The complete app flow test would look like this:

testWidgets("Test to Check the Complete app flow", (tester) async {
    await tester.pumpWidget(const MaterialApp(home: HomePage()));
    await tester.pumpAndSettle();
    addDelay(2000);

    // Open the fist image in Full Screen (Keys assigned in the HomePage, GridView) 
    await tester.tap(find.byKey(const ValueKey(0)));
    await tester.pumpAndSettle();
    addDelay(1000);

    // Go Back to HomePage
    await tester.tap(find.byIcon(Icons.arrow_back));
    await tester.pumpAndSettle();

    // Go to Search Page
    await tester.tap(find.byType(FloatingActionButton));
    await tester.pumpAndSettle();
    addDelay(1000);
 
    // Assert to check we are on the right page
    expect(find.byType(TextField), findsOneWidget);


    // Enter Text and make query
    await tester.enterText(find.byType(TextField), "Trees");
    await tester.tap(find.byKey(SearchPage.searchIconButtonKey));

    // Wait to get results and use pump and settle to update interface
    addDelay(2000);
    await tester.pumpAndSettle();
    
    // Assert that we get Results
    expect(find.byType(Image), findsWidgets);
    addDelay(2000);
  });

The integration test runs in a real device, so ensure to have a device connected. We have several delays without any reason here just to ensure we are able to see the tester operate the application, as the operator works very fast. So that’s it for testing in Flutter.

Conclusion πŸ₯£

In this post, we discussed the three types of testing in Flutter. We looked at live examples as to how to use unit, widget, and integration testing in Flutter. We discussed the concept of arranging, acting, and asserting and also looked at how to use the expect method to assert conditions.

I hope now you will be able to write tests in Flutter easily and fluently. The complete source code is available on Github. You must check these detailed articles I wrote:

If you have any queries you can comment below and I will be happy to help you. If you are new to Flutter check my post on how to install Flutter on windows?

Leave a Reply

Your email address will not be published.

Previous Post
GetX in Flutter

Complete guide to GetX in Flutter

Next Post
Custom ValueNotifier in Flutter

Custom ValueNotifier in Flutter – With Examples

Related Posts