In Flutter development, writing effective unit and widget tests is crucial for ensuring the reliability and maintainability of your applications. Mocking and stubbing are powerful techniques used in testing to isolate the component under test by replacing its dependencies with controlled substitutes. This approach enables you to verify that the component interacts correctly with its dependencies and handles various scenarios predictably.
What is Mocking and Stubbing?
Mocking: Mocking involves creating mock objects that simulate the behavior of real dependencies. These mock objects allow you to define how they should be called and what they should return, thereby controlling the inputs and outputs of the component being tested.
Stubbing: Stubbing is a technique used to replace real dependencies with simple, pre-programmed responses. A stub provides fixed answers to method calls, which helps isolate the code under test from external dependencies without fully replicating their behavior.
Why Use Mocking and Stubbing in Flutter Tests?
- Isolation: Isolate the component being tested from its dependencies.
- Predictability: Ensure consistent and predictable test results by controlling the behavior of dependencies.
- Speed: Speed up tests by replacing slow or complex dependencies with lightweight mocks or stubs.
- Verification: Verify that the component interacts correctly with its dependencies.
How to Implement Mocking and Stubbing in Flutter Tests
To implement mocking and stubbing in Flutter, you can use popular packages such as mockito or mocktail.
Step 1: Add Dependencies
First, add the mockito or mocktail package to your dev_dependencies in pubspec.yaml:
Using mockito:
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.0.0
Using mocktail:
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^1.0.0
Run flutter pub get to install the dependencies.
Step 2: Create a Mock Class
Create a mock class for the dependency you want to replace in your tests.
Using mockito:
import 'package:mockito/mockito.dart';
import 'package:your_app/services/api_service.dart';
// Define a class that the mock will implement
class ApiService {
Future<String> fetchData() async {
throw UnimplementedError();
}
}
class MockApiService extends Mock implements ApiService {}
Using mocktail:
import 'package:mocktail/mocktail.dart';
import 'package:your_app/services/api_service.dart';
class ApiService {
Future<String> fetchData() async {
throw UnimplementedError();
}
}
class MockApiService extends Mock implements ApiService {}
Step 3: Use the Mock in Your Tests
In your test file, instantiate the mock and define its behavior using when and thenReturn or thenAnswer.
Using mockito:
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:your_app/my_widget.dart';
import 'package:your_app/services/api_service.dart';
void main() {
group('MyWidget', () {
testWidgets('fetches data correctly', (WidgetTester tester) async {
// Arrange
final mockApiService = MockApiService();
when(mockApiService.fetchData()).thenAnswer((_) => Future.value('Mocked data'));
// Act
await tester.pumpWidget(MyWidget(apiService: mockApiService));
await tester.pumpAndSettle();
// Assert
expect(find.text('Mocked data'), findsOneWidget);
verify(mockApiService.fetchData()).called(1);
});
});
}
Using mocktail:
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:your_app/my_widget.dart';
import 'package:your_app/services/api_service.dart';
void main() {
group('MyWidget', () {
testWidgets('fetches data correctly', (WidgetTester tester) async {
// Arrange
final mockApiService = MockApiService();
when(() => mockApiService.fetchData()).thenAnswer((_) => Future.value('Mocked data'));
// Act
await tester.pumpWidget(MyWidget(apiService: mockApiService));
await tester.pumpAndSettle();
// Assert
expect(find.text('Mocked data'), findsOneWidget);
verify(() => mockApiService.fetchData()).called(1);
});
});
}
Complete Example
Let’s look at a complete example with a simple widget that fetches data from an API and displays it.
ApiService
// api_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiService {
Future<String> fetchData() async {
final response = await http.get(Uri.parse('https://api.example.com/data'));
if (response.statusCode == 200) {
return jsonDecode(response.body)['data'];
} else {
throw Exception('Failed to load data');
}
}
}
MyWidget
// my_widget.dart
import 'package:flutter/material.dart';
import 'package:your_app/services/api_service.dart';
class MyWidget extends StatefulWidget {
final ApiService apiService;
MyWidget({Key? key, required this.apiService}) : super(key: key);
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
String data = 'Loading...';
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
try {
final fetchedData = await widget.apiService.fetchData();
setState(() {
data = fetchedData;
});
} catch (e) {
setState(() {
data = 'Error: ${e.toString()}';
});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('My Widget'),
),
body: Center(
child: Text(data),
),
),
);
}
}
Test File
Using mockito:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:your_app/my_widget.dart';
import 'package:your_app/services/api_service.dart';
// Create a mock for the ApiService
class MockApiService extends Mock implements ApiService {}
void main() {
testWidgets('MyWidget fetches and displays data', (WidgetTester tester) async {
// Arrange
final mockApiService = MockApiService();
when(mockApiService.fetchData()).thenAnswer((_) => Future.value('Mocked data'));
// Act
await tester.pumpWidget(MyWidget(apiService: mockApiService));
await tester.pumpAndSettle();
// Assert
expect(find.text('Mocked data'), findsOneWidget);
verify(mockApiService.fetchData()).called(1);
});
testWidgets('MyWidget handles error when fetching data', (WidgetTester tester) async {
// Arrange
final mockApiService = MockApiService();
when(mockApiService.fetchData()).thenThrow(Exception('Failed to load data'));
// Act
await tester.pumpWidget(MyWidget(apiService: mockApiService));
await tester.pumpAndSettle();
// Assert
expect(find.textContaining('Error:'), findsOneWidget);
verify(mockApiService.fetchData()).called(1);
});
}
Using mocktail:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:your_app/my_widget.dart';
import 'package:your_app/services/api_service.dart';
// Create a mock for the ApiService
class MockApiService extends Mock implements ApiService {}
void main() {
testWidgets('MyWidget fetches and displays data', (WidgetTester tester) async {
// Arrange
final mockApiService = MockApiService();
when(() => mockApiService.fetchData()).thenAnswer((_) => Future.value('Mocked data'));
// Act
await tester.pumpWidget(MyWidget(apiService: mockApiService));
await tester.pumpAndSettle();
// Assert
expect(find.text('Mocked data'), findsOneWidget);
verify(() => mockApiService.fetchData()).called(1);
});
testWidgets('MyWidget handles error when fetching data', (WidgetTester tester) async {
// Arrange
final mockApiService = MockApiService();
when(() => mockApiService.fetchData()).thenThrow(Exception('Failed to load data'));
// Act
await tester.pumpWidget(MyWidget(apiService: mockApiService));
await tester.pumpAndSettle();
// Assert
expect(find.textContaining('Error:'), findsOneWidget);
verify(() => mockApiService.fetchData()).called(1);
});
}
Best Practices
- Use Specific Mocks: Create mocks that are specific to the test case. Avoid creating generic mocks that can be used in multiple scenarios.
- Verify Interactions: Verify that the component interacts with its dependencies in the expected way.
- Avoid Over-Mocking: Mock only the external dependencies that are necessary for testing the component. Avoid mocking internal methods or properties.
- Keep Tests Readable: Ensure that your tests are easy to read and understand. Use meaningful names for mocks and assertions.
Conclusion
Mocking and stubbing are essential techniques for writing effective and reliable tests in Flutter. By isolating components and controlling their dependencies, you can ensure that your tests are predictable, fast, and accurate. Leveraging libraries such as mockito and mocktail makes it easier to create and manage mocks in your Flutter projects, leading to more robust and maintainable applications.