Mastering Mocks: A Deep Dive into Unit Testing with Dart

Uncover the Secrets of Mocking Classes for Seamless and Effective Testing

Mastering Mocks: A Deep Dive into Unit Testing with Dart

Introduction

Welcome to the second part of the Unit Testing series in Flutter. Previously in the first part we discussed the basics of Unit testing and real life problems. This second part will be focused on Mocking custom classes for effective testing.

Mocking is a technique which allows us to create fake or simulated versions of classes and functions. These mocks simulate the behavior of the real class, enabling developers to control and observe how their class interacts with them without relying on the actual implementations.

For example, in Dart and Flutter, if your app interacts with an external API service, you might create a mock version of that service for testing. This mock service behaves similarly to the real service, allowing you to test your app's behavior without making actual API calls, ensuring more predictable and controlled testing scenarios. Before starting, ensure that your Flutter project has the following in the dev dependencies.

dev_dependencies:
  flutter_test:
    sdk: flutter

There are two popular packages we can use to create mocks for our classes, so we will see them one by one. First install mocktail package -

dev_dependencies:
  flutter_test:
    sdk: flutter
  mocktail: ^1.0.1

So first we are going to write a unit test to test the following code -

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

class UserRepository {
  final http.Client client;
  UserRepository(this.client);

  Future<User> getUser() async {
    final response = await client.get(
      Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
    );
    if (response.statusCode == 200) {
      return User.fromJson(response.body);
    }
    throw Exception('Some Error Occurred');
  }
}

Unit test of UserRepository -

import 'package:http/http.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
import 'package:unit_tests/user_model.dart';
import 'package:unit_tests/user_repository.dart';

Client client = Client();

class MockHttpClient extends Mock implements Client {}

void main() {
  late UserRepository userRepository;
  late MockHttpClient mockHttpClient;

  setUp(() {
    mockHttpClient = MockHttpClient();
    userRepository = UserRepository(mockHttpClient);
  });

  group(
    "UserRepository - ",
    () {
      group(
        'GetUser Function - ',
        () {
          test(
            'If getUser function return status code 200 then usermodel should be returned',
            () async {
              when(
                () => mockHttpClient.get(
                  Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
                ),
              ).thenAnswer(
                (invocation) async {
                  return Response(
                    ''' 
                    {
                      "id": 1,
                      "name": "Leanne Graham",
                      "username": "Bret",
                      "email": "Sincere@april.biz",
                      "website": "hildegard.org"
                    }
                    ''',
                    200,
                  );
                },
              );

              final user = await userRepository.getUser();

              expect(user, isA<User>());
            },
          );

          test(
            'If getuser Function return status code which is not 200 then exception must be thrown',
            () async {
              when(
                () => mockHttpClient.get(
                  Uri.parse('https://jsonplaceholder.typicode.com/users/1'),
                ),
              ).thenAnswer(
                (invocation) async {
                  return Response(
                    ''' ''',
                    404,
                  );
                },
              );

              final user = userRepository.getUser();

              expect(user, throwsException);
            },
          );
        },
      );
    },
  );
}

You can find the entire code here.

Now let’s take a look at the Unit Test -

class MockHttpClient extends Mock implements Client {}: Creating a mock class MockHttpClient that extends Mock and implements the Client interface. This will be used to replace the real HTTP client during testing.

mockHttpClient = MockHttpClient();: Creating an instance of the MockHttpClient.

userRepository = UserRepository(mockHttpClient);: Creating an instance of UserRepository with the mock HTTP client.

Test case 1 -

Checks if the getUser function of UserRepository returns a User model when the HTTP client returns a status code of 200. It uses the when method to specify the expected behavior of the mock HTTP client.

Test case 2 -

Checks if the getUser function throws an exception when the HTTP client returns a status code other than 200.

Now let’s try to test a bit more complex piece of code using the mockito package. For that include mockito package -

dev_dependencies:
  flutter_test:
    sdk: flutter
  http_mock_adapter: ^0.6.1
  mockito: ^5.4.4

Here’s the code that we need to test using mockito -

class QuizService {
  QuizService({required Dio client}) : _client = client;
  final Dio _client;

    FutureAppEither<List> loadEntertainmentQuizes(int category) async {
    try {
      const url = 'https://opentdb.com/api.php';

      final response = await _client.get(
        url,
        queryParameters: {
          'category': category,
          'difficulty': 'easy',
          'amount': 6,
        },
      );
      log('Response Code From FetchQuiz : ${response.statusCode}');
      if (response.statusCode == 200) {
        return right(response.data['results']);
      } else {
        return left(
          AppError(message: 'Something went wrong!'),
        );
      }
    } catch (error) {
      log('Error Message in QuizService : $error');
      return left(
        AppError(message: 'Something went wrong!'),
      );
    }
  }
}

We have considered a more complex use case which uses dio, fpdart, etc packages.

void main() {
  const openTDBUrl = 'https://opentdb.com/api.php';
  const quizApiQueryParam = {
    'apiKey': '13tmNQnZ9tysUNHRI6X533wej3BOzF0o4ilK6Nee',
    'tags': 'Programming',
    'limit': 6,
  };
  const openTDBQueryParam = {
    'category': 9,
    'difficulty': 'easy',
    'amount': 6,
  };
  late QuizService quizService;
  late Dio dio;
  late DioAdapter dioAdapter;

  setUpAll(() {
    dio = Dio();
    dioAdapter = DioAdapter(dio: dio);
    quizService = QuizService(client: dio);
  });

  group(
    'Fetch Quizes from opentdb.com -',
    () {
      test(
        'should return List of Quizes when the response code is 200',
        () async {
          dioAdapter.onGet(
            openTDBUrl,
            queryParameters: openTDBQueryParam,
            (server) {
              return server.reply(
                200,
                {
                  "response_code": 0,
                  "results": [
                    {
                      "type": "multiple",
                      "difficulty": "easy",
                      "category": "General Knowledge",
                      "question":
                          "What is the closest planet to our solar system&#039;s sun?",
                      "correct_answer": "Mercury",
                      "incorrect_answers": ["Mars", "Jupiter", "Earth"]
                    },
                  ],
                },
              );
            },
          );

          final result = await quizService.loadEntertainmentQuizes(9);

          final isRight = result.isRight();
          expect(isRight, true);

          final isSuccess = result.toNullable();
          expect(isSuccess, isA<List>());

          final error = result.getLeft().toNullable();
          expect(error, null);
        },
      );

      test(
        'should return AppError when the response code is not 200',
        () async {
          dioAdapter.onGet(
            openTDBUrl,
            queryParameters: openTDBQueryParam,
            (server) {
              return server.reply(
                400,
                'Something went wrong!',
              );
            },
          );

          final result = await quizService.loadEntertainmentQuizes(9);

          final isLeft = result.isLeft();
          expect(isLeft, true);

          final error = result.getLeft().toNullable();
          expect(error, isA<AppError>());
        },
      );
    },
  );
}

First we define all the required parameters and variables which are going to be used in the test case.

Test Case 1 -

This expects the loadEntertainmentQuizes method to return a list of quizzes when the HTTP response code is 200. It uses dioAdapter.onGet to mock the response with a status code of 200 and a sample quiz.

Verifying the result using expect statements:

  • Checking if the result is a Right (indicating success).

  • Checking if the result, when converted to a nullable value, is a List.

  • Verifying that there is no left (error) value.

Test Case 2 -

This expects the loadEntertainmentQuizes method to return an AppError when the HTTP response code is not 200. It uses dioAdapter.onGet to mock the response with a status code of 400 and an error message.

Verifying the result using expect statements:

  • Checking if the result is a Left (indicating an error).

  • Checking if the left (error) value, when converted to a nullable value, is an instance of AppError.

The dioAdapter is crucial in simulating these responses without making actual HTTP requests during testing.

Embrace the power of mock testing, and watch as your Flutter projects not only meet but exceed expectations. Remember, it's the key to crafting software that stands out in its performance and reliability, making your development journey truly exceptional.

Happy Fluttering 💙💙💙

Did you find this article valuable?

Support Pranav Masekar by becoming a sponsor. Any amount is appreciated!