Utilizing Mocking and Stubbing Libraries Like Mockito to Isolate Dependencies and Create Testable Code in Flutter

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 MockHttpClient is created using extends Mock implements http.Client.
  • The behavior of the mockClient is stubbed using when and thenAnswer to simulate successful and error responses.
  • The expect function asserts that the NewsService behaves 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 the mockDataService with 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.