Practical Unit Test in Flutter with Mockito

Dwi Randy Herdinanto
7 min readJan 7, 2023

--

Introduction Unit Testing

Unit testing is a software testing method where individual units or components of a software application are tested in isolation from the rest of the application. The goal of unit testing is to validate that each unit or component of the application is working as intended and meets the requirements.

In Flutter Unit tests are handy for verifying the behavior of a single function, method, or class. The test package provides the core framework for writing unit tests, and the flutter_test package provides additional utilities for testing widgets.

In this tutorial, we’ll learn how to use Mockito to test Flutter code. We’l learn how to generate mocks, stub data, and perform tests. Let’s get started!

Starter Project

To learn how to do unit testing on Flutter applications, here I have prepared a starter project, this Starter Project is a simple example where the application will retrieve data from the server and display it in a ListView.

The project structure

lib
- model # Object that represent json from API
- article.dart
- news_response.dart
- source.dart
- provider # State management
- news_provider.dart
- service # Service class for fetching data from API
- news_api_service.dart
- main.dart # UI

The starter project can be checked in this repository, then we will do unit testing on this starter project.

What is Mockito

Before we start to learn about mockito, we need to know first that the unit we want to test must be tested in isolation from the rest of the application. the class or unit that we want to test needs to be completely isolated from any other class or any other system.

To unit test a class, you need to ensure that all of its dependencies can be replaced with fake versions that can be tested without relying on any external entities.
If you are unable to do this and need to use real external dependencies to test the class. Unit tests that depend on data from live web services or databases can be inconvenient because:

  • Calling live services or databases slows down test execution.
  • A passing test might start failing if a web service or database returns unexpected results. This is known as a “flaky test.”
  • It is difficult to test all possible success and failure scenarios by using a live web service or database.

That’s why we can consider using mockito. Mockito allows us to create mock objects / external dependencies and define their behavior in a concise and intuitive way. This makes it easy to write unit tests that focus on the functionality of the class being tested, without being affected by the behavior of its dependencies.

Install Mockito and Create a Mock Object

We need to add mockito and build_runner dependencies to our pubspec.yaml

dependencies:
flutter:
sdk: flutter
...

dev_dependencies:
flutter_test:
sdk: flutter

mockito: ^5.0.8
build_runner: ^2.0.4

Don’t forget to run flutter pub get to download these dependencies into our project

Unit Test Class

In general, test files should reside inside a test folder located at the root of your Flutter application or package. Test files should always end with _test.dart, this is the convention used by the test runner when searching for tests.

lib
- provider
- services
test
- news_api_service_test.dart
- news_provider_test.dart

Add Unit Test for news_api_service.dart

API Service is a class that is used to interact with the API, in this case the news_api_service.dart will request News data to the server.

We will do unit testing in this class to make sure the behavior of the class is as we want. If successful calling the data method will get the changed object List<Article> otherwise this class will return an Error.

Here is and implementation of news_api_service.dart

class NewsApiService {
final http.Client client;

NewsApiService(this.client);

Future<List<Article>> fetchArticle() async {
final uri = Uri.parse(
'$baseUrl/everything?q=flutter&apiKey=788576fa85e0490eacac2d580771d924');
final response = await client.get(uri);
if (response.statusCode == 200) {
return NewsResponse.fromJson(json.decode(response.body)).articles;
} else {
throw Error();
}
}
}

Create a test class

Inside folder test we create a test class named news_api_service_test.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:http/http.dart' as http;

@GenerateMocks([http.Client])
void main() {
late MockClient mockClient;
late NewsApiService newsApiService;

setUp(() {
mockClient = MockClient();
newsApiService = NewsApiService(mockClient);
});

}

As we can see above there is @GenerateMocks that annotation to informs the build runner to generate mocks for all the classes in the list. Since we want to isolate the NewsApiService we need to mock Client that way we can manipulate the behavior of the Client object.

Next, open the terminal and run the command flutter pub run build_runner build to start generating mocks for the classes. After code generation is complete, we’ll be able to access the generated mocks by prepending Mock to the class name.

Add a test case below setUp method, in this scenario

  final uri = Uri.parse(
'$baseUrl/everything?q=flutter&apiKey=788576fa85e0490eacac2d580771d924');
const jsonString = """
{
"status": "ok",
"totalResults": 484,
"articles": [{
"source": {
"id": null,
"name": "ReadWrite"
},
"author": "Chris Gale",
"title": "What Drives Choosing Flutter Over React Native?",
"description": "For those looking at open-source options",
"url": "https://readwrite.com/what-drives-choosing-flutter-over-react-native/",
"urlToImage": "https://images.readwrite.com/wp-content/uploads/2022/11/Super-hero-3497522.jpg",
"publishedAt": "2022-12-09T16:00:37Z",
"content": "For those looking at open-source options for applications"
}]
}
""";

final articles = [
Article(
source: Source(id: null, name: "ReadWrite"),
author: "Chris Gale",
title: "What Drives Choosing Flutter Over React Native?",
description: "For those looking at open-source options",
url:
"https://readwrite.com/what-drives-choosing-flutter-over-react-native/",
urlToImage:
"https://images.readwrite.com/wp-content/uploads/2022/11/Super-hero-3497522.jpg",
publishedAt: DateTime.parse("2022-12-09T16:00:37Z"),
content: "For those looking at open-source options for applications",
)
];

test('should return a list of articles with success when status code is 200',
() async {
// arrange
when(mockClient.get(uri))
.thenAnswer((_) async => http.Response(jsonString, 200));
// act
final result = await newsApiService.fetchArticle();
// assert
expect(result, equals(articles));
verify(mockClient.get(uri));
});

In Flutter, a stub is a fake object that is returned when a mock method is called during a test.

when: Creates a stub method response. Mockito will store the fake call and pair the exact arguments given with the response. The response generators from Mockito includethenReturn, thenAnswer, and thenThrow.

thenAnswer: Stores a function which is called when this method stub is called. The function will be called, and the return value will be returned.

So when we run that test, NewsApi Service will get a json that we will get our prepared json string instead of calling from the API

We return the List<Article> from the stub and use the expect to assert that actual matches matcher. We also verify that method get was called

Next we will make a test to test the fetch Article method will return an Error when the API status code is not 200

test('should throw an error when status code is not 200', () async {
// arrange
when(mockClient.get(uri)).thenAnswer((_) async => http.Response('', 404));
// act
final call = newsApiService.fetchArticle();
// assert
expect(() => call, throwsA(isA<Error>()));
verify(mockClient.get(uri));
});

We call the mockClient.get(uri) using the when and return the stub response as the Response('', 404) 404 is the code to indicate resources not found

Next, we store a method of fetchArticle() into call variable

Finally, we expect when the fetchArticle() method is executed it will return an Error, since we are setting the response from the mockClient.get()to Response('', 404).

Add Unit Test for news_provider.dart

In Flutter, the provider is a popular state management solution that allows developers to create objects that can be shared and provided to other widgets throughout the app.

This is the implementation code of news_provider.dart in our current project

enum ResultState { success, failed, loading }

class NewsProvider extends ChangeNotifier {
final NewsApiService apiService;

List<Article> articles = [];
ResultState state = ResultState.loading;

NewsProvider(this.apiService);

void loadNews() async {
state = ResultState.loading;
notifyListeners();

try {
articles = await apiService.fetchArticle();
state = ResultState.success;
} catch (error) {
state = ResultState.failed;
}

notifyListeners();
}
}

We will first Initialize MockNewsApiService & NewsProvider inside setupUp()

@GenerateMocks([NewsApiService])
void main() {
late MockNewsApiService mockNewsApiService;
late NewsProvider newsProvider;
int listenCount = 0;

setUp(() {
listenCount = 0;
mockNewsApiService = MockNewsApiService();
newsProvider = NewsProvider(mockNewsApiService)
..addListener(() {
listenCount++;
});
});

final articles = [
Article(
source: Source(id: null, name: "ReadWrite"),
author: "Chris Gale",
title: "What Drives Choosing Flutter Over React Native?",
description: "For those looking at open-source options",
url:
"https://readwrite.com/what-drives-choosing-flutter-over-react-native/",
urlToImage:
"https://images.readwrite.com/wp-content/uploads/2022/11/Super-hero-3497522.jpg",
publishedAt: DateTime.parse("2022-12-09T16:00:37Z"),
content: "For those looking at open-source options for applications",
)
];

test(
'should change state into success, and articles variable should be filled',
() async {
// arrange
when(mockNewsApiService.fetchArticle()).thenAnswer((_) async => articles);
// act
await newsProvider.loadNews();
// assert
expect(newsProvider.state, equals(ResultState.success));
expect(newsProvider.articles, equals(articles));
expect(listenCount, equals(2));
verify(mockNewsApiService.fetchArticle());
});
}

We call the mockNewsApiService.fetchArticle() using the when and return the stub response as the articles which is List<Article>

Next, we call method newsProvider.loadNews() it will call mockNewsApiService.fetchArticle()

Finally, we expect when the loadNews() is executed it will fill variable articles inside NewsProvider then set state into ResultState.success . We also expect that provider will call notifyListener() twice

Let’s go to the next test

test('should change state into error', () async {
// arrange
when(mockNewsApiService.fetchArticle()).thenThrow(Error());
// act
await newsProvider.loadNews();
// assert
expect(newsProvider.state, equals(ResultState.failed));
expect(listenCount, equals(2));
verify(mockNewsApiService.fetchArticle());
});

We call the mockNewsApiService.fetchArticle() using the when and return the stub throw an Error()

Next, we call method newsProvider.loadNews() it will call mockNewsApiService.fetchArticle()

Finally, we expect when the loadNews() is executed it will set state into ResultState.failed we also expect that provider will call notifyListener() twice

Run All Unit Test

After we make a unit test for the api service and also the provider. we can run all the tests with the flutter test test command and the result is as below, all tests were successfully run and in line with our expectations.

Conclusion

In summary, it is important to write tests using Mockito in order to minimize the need for manual QA, especially as the size of the app grows.

This article provided an overview of using Mockito to create mock objects and using fakes and argument matchers for functional testing. We also discussed how to structure the app to make mocking more efficient.

You can download the source code in this article in my repository

--

--

Dwi Randy Herdinanto
Dwi Randy Herdinanto

Written by Dwi Randy Herdinanto

A software developer that enthusiastic about mobile applications

Responses (2)