In Flutter development, writing robust and maintainable code involves creating comprehensive unit tests. However, testing components in isolation can be challenging when they depend on external services, APIs, or complex business logic. This is where mocking and stubbing libraries like Mockito come into play. By isolating dependencies and simulating their behavior, Mockito enables developers to write focused and reliable unit tests. In this blog post, we will explore how to utilize Mockito in Flutter to isolate dependencies and create highly testable code.
Understanding the Basics: Mocking and Stubbing
Before diving into the implementation, let’s define mocking and stubbing and their roles in unit testing:
- Mocking: Creating objects that simulate the behavior of real dependencies. These mock objects allow you to verify interactions and ensure your code is interacting correctly with its dependencies.
- Stubbing: Configuring mock objects to return specific values or perform specific actions when certain methods are called. This allows you to control the behavior of dependencies in a predictable way.
Why Use Mockito in Flutter?
Mockito offers several advantages for unit testing in Flutter:
- Dependency Isolation: Allows you to isolate the component under test from its dependencies, making tests more focused and reliable.
- Simplified Testing: Simplifies the testing process by providing an easy way to simulate complex behaviors of dependencies.
- Increased Testability: Encourages writing more testable code by promoting loose coupling between components.
- Interaction Verification: Enables you to verify that specific methods are called on dependencies with certain arguments, ensuring correct interactions.
Setting Up Mockito in Flutter
To start using Mockito in your Flutter project, follow these steps:
Step 1: Add Dependencies to pubspec.yaml
Add the necessary dependencies to your pubspec.yaml file:
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.1.0
build_runner: ^2.0.0 # For code generation
Ensure to run flutter pub get to fetch the dependencies.
Step 2: Configure build_runner
Mockito uses code generation to create mock objects. You can trigger the code generation by running:
flutter pub run build_runner build
Or, to watch for changes and rebuild automatically, run:
flutter pub run build_runner watch
Writing Unit Tests with Mockito
Let’s explore a practical example of how to use Mockito in a Flutter unit test. Consider a scenario where a NewsService depends on an HttpClient to fetch news articles.
Scenario: Testing NewsService with Mockito
First, define the NewsService and HttpClient:
import 'dart:convert';
import 'package:http/http.dart' as http;
class NewsService {
final http.Client client;
NewsService(this.client);
Future> getNewsArticles() async {
final response = await client.get(Uri.parse('https://example.com/news'));
if (response.statusCode == 200) {
final List data = jsonDecode(response.body);
return data.map((item) => item['title'].toString()).toList();
} else {
throw Exception('Failed to load news articles');
}
}
}
Now, let’s create a mock HttpClient using Mockito and test the NewsService.
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
import 'news_service.dart';
// Generate a mock class for the http.Client.
// To do this, create a new class that extends Mock
// and implements the class you want to mock.
class MockHttpClient extends Mock implements http.Client {}
void main() {
group('NewsService', () {
test('fetches news articles successfully', () async {
final mockClient = MockHttpClient();
final newsService = NewsService(mockClient);
// Stub the mock client to return a successful response
when(mockClient.get(Uri.parse('https://example.com/news')))
.thenAnswer((_) async => http.Response(
jsonEncode([
{'title': 'Article 1'},
{'title': 'Article 2'}
]),
200,
));
final articles = await newsService.getNewsArticles();
expect(articles, ['Article 1', 'Article 2']);
});
test('throws an exception if the http call completes with an error', () async {
final mockClient = MockHttpClient();
final newsService = NewsService(mockClient);
// Stub the mock client to return an error response
when(mockClient.get(Uri.parse('https://example.com/news')))
.thenAnswer((_) async => http.Response('Not Found', 404));
expect(newsService.getNewsArticles(), throwsA(isA()));
});
});
}
In this example:
- A
MockHttpClientis created usingextends Mock implements http.Client. - The behavior of the
mockClientis stubbed usingwhenandthenAnswerto simulate successful and error responses. - The
expectfunction asserts that theNewsServicebehaves as expected for different scenarios.
Mockito Features: Real-World Examples
1. Verifying Interactions
Verify that methods are called on the mock object.
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'news_service.dart';
class MockDataService extends Mock implements DataService {}
class DataConsumer {
final DataService dataService;
DataConsumer(this.dataService);
void consumeData(String id) {
dataService.fetchData(id);
}
}
class DataService {
Future fetchData(String id) async {
await Future.delayed(Duration(seconds: 1));
print('Fetched data with ID: $id');
}
}
void main() {
test('verify method calls on mock', () {
final mockDataService = MockDataService();
final dataConsumer = DataConsumer(mockDataService);
dataConsumer.consumeData("123");
// Verify that fetchData was called with "123"
verify(mockDataService.fetchData("123")).called(greaterThan(0));
// Verify that fetchData was only called once
verify(mockDataService.fetchData("123")).called(1);
});
}
- This example demonstrates verifying interactions with the mocked DataService
- Verify calls made to
fetchData()on themockDataServicewith specified parameters.
2. Stubbing Method Chains
Define the behavior of multiple methods calls in chain.
class MockUserService extends Mock implements UserService {}
class User {
String getName() {
return "User";
}
Address getAddress() {
return Address();
}
}
class Address {
String getCity() {
return "City";
}
}
class UserService {
User getUser(String id) {
return User();
}
}
void main() {
test('stubbing method chains', () {
final mockUserService = MockUserService();
when(mockUserService.getUser("123").getAddress().getCity()).thenReturn("TestCity");
expect(mockUserService.getUser("123").getAddress().getCity(), equals("TestCity"));
});
}
3. Mocking Streams
Mockito allows you to mock reactive streams using StreamController
import 'dart:async';
import 'package:mockito/mockito.dart';
import 'package:flutter_test/flutter_test.dart';
class MockStreamService extends Mock implements StreamService {}
class StreamService {
Stream getCounterStream() {
return Stream.periodic(Duration(seconds: 1), (i) => i);
}
}
class StreamConsumer {
final StreamService streamService;
StreamConsumer(this.streamService);
Stream consumeStream() {
return streamService.getCounterStream();
}
}
void main() {
test('mocking streams', () async {
final mockStreamService = MockStreamService();
final streamConsumer = StreamConsumer(mockStreamService);
final streamController = StreamController();
when(mockStreamService.getCounterStream()).thenAnswer((_) => streamController.stream);
// Add values to the stream
streamController.add(1);
streamController.add(2);
streamController.add(3);
await streamController.close();
// Start listening
final stream = streamConsumer.consumeStream();
// Collect results from stream
final results = await stream.toList();
expect(results, equals([1, 2, 3]));
});
}
Conclusion
Mockito is a powerful library for creating testable code in Flutter by enabling developers to isolate dependencies and simulate their behavior. By following the guidelines and examples in this blog post, you can effectively utilize Mockito to write comprehensive unit tests, improve the quality of your Flutter applications, and achieve higher levels of code reliability. Incorporating Mockito into your testing workflow can lead to better code organization, reduced debugging efforts, and enhanced maintainability.