How to Mock HTTP Clients in Flutter Tests

During testing, it is important to avoid making actual HTTP calls. Instead, you can mock the HTTP client. This approach allows you to control the responses and ensure your tests remain isolated. Additionally, your tests can run offline and will not disrupt your pipelines. In this post, we will mock HTTP clients in Flutter tests.

Create an API Client

To get started, we will create an API client that we will be testing. The API client will have two functions that retrieve post data from the JSONPlaceholder API.

Installing Http package

To send HTTP calls we are going to use the Http package. We can install the package by executing the following command inside our project:

flutter pub add http

Once the command is executed, make sure to check your pubspec.yaml file for the added dependencies. You should see the Http package included in the dependencies section, like this:

dependencies:
  http: ^1.2.1

Create the PostClient API Client

For the API Client, we create a new class called PostClient. In the PostClient class, we will have the posts and post functions. The posts function will retrieve all the posts, and the post function will retrieve a post by ID.

import 'dart:convert';

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

void main() async {
  final posts = await PostClient().posts();
  final post = await PostClient().post(1);

  print(posts);
  print(post);
}

class PostClient {
  final _baseUrl = 'https://jsonplaceholder.typicode.com/posts';

  Future<List<dynamic>> posts() async {
    final request = Uri.parse(_baseUrl);
    final response = await http.Client().get(request);

    return jsonDecode(response.body) as List;
  }

  Future<Map<String, dynamic>> post(int id) async {
    final request = Uri.parse('$_baseUrl/$id');
    final response = await http.Client().get(request);

    return jsonDecode(response.body) as Map<String, dynamic>;
  }
}

In the above code snippet, we have created the PostClient class. The class has a _baseUrl attribute which is the base URL of our API. In the class, we have two functions that both make an API call using the base URL. The posts function returns a List while the post function returns a Map. The post function also takes an id which is used to fetch a post by ID.

When you run the main.dart file, you will see that we retrieve 100 posts by calling the posts function and a single post by calling the post function with an ID of 1.

The JSON structure of each post is as follows:

{
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },

Normally you should deserialize the received JSON data into Dart objects. However, in this post, we want to focus on testing, therefore, we keep the data as it is. If you want to learn more about deserialization check out the following article: How to Deserialize JSON Responses in Flutter

Create API Client Test

After creating our API Client, we can now write our first test case. Because the class is called PostClient we will create a test file called post_client_test.dart. Inside this file, we will write all our PostClient tests.

import 'package:flutter_test/flutter_test.dart';
import 'package:mock_http_clients/main.dart';

void main() {
  test('Fetches all posts successfully', () async {
    final posts = await PostClient().posts();

    print(posts);

    expect(posts.length, 100);
  });
}

In the above code, we created a simple test that calls the posts function from our PostClient. For now, we will print the result. However, remember to never keep print statements in your actual tests. Finally, we check if the length of the results from our posts function is equal to 100.

If you run this test, you will notice that it succeeds. However, we are currently making an actual HTTP call, which should be avoided. Let us move on to mocking our HTTP client.

Install Mocktail Package

To mock our HTTP client we will be using the Mocktail package. We can install the package by executing the following command inside our project:

flutter pub add mocktail --dev

Once the command is executed, make sure to check your pubspec.yaml file for the added dependencies. You should see the Mocktail package included in the dependencies section, like this:

dev_dependencies:
  mocktail: ^1.0.4

Add Constructor to the PostClient

After installing the Mocktail package, we first have to make a change to our PostClient class. Because we will be mocking the Client from the Http package. We need a way to pass the mocked client to our PostClient class.

import 'dart:convert';

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

void main() async {
  final posts = await PostClient().posts();
  final post = await PostClient().post(1);

  print(posts);
  print(post);
}

class PostClient {
  PostClient({http.Client? httpClient})
      : _httpClient = httpClient ?? http.Client();

  final http.Client _httpClient;
  final _baseUrl = 'https://jsonplaceholder.typicode.com/posts';

  Future<List<dynamic>> posts() async {
    final request = Uri.parse(_baseUrl);
    final response = await _httpClient.get(request);

    return jsonDecode(response.body) as List;
  }

  Future<Map<String, dynamic>> post(int id) async {
    final request = Uri.parse('$_baseUrl/$id');
    final response = await _httpClient.get(request);

    return jsonDecode(response.body) as Map<String, dynamic>;
  }
}

In the following code, we added a constructor to our PostClient class. The constructor now has an optional parameter to pass a Client. When the parameter is not used we will assign the default Client from the Http package.

After making this change, we can now continue testing the class with a mocked HTTP client.

Use Mocktail Inside our Test Cases

To mock the HTTP client we need to make a lot of changes to our test case, see the code below.

import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:mock_http_clients/main.dart';
import 'package:mocktail/mocktail.dart';

class MockHttpClient extends Mock implements Client {}

class MockResponse extends Mock implements Response {}

void main() {
  final Client httpClient = MockHttpClient();
  final Response response = MockResponse();
  final PostClient postClient = PostClient(httpClient: httpClient);

  setUp(() async {
    when(() => response.statusCode).thenReturn(200);
    when(() => response.body).thenReturn(
      jsonEncode([
        {"userId": 1, "id": 1, "title": "test", "body": "test"},
        {"userId": 1, "id": 2, "title": "test", "body": "test"},
      ]),
    );

    when(() => httpClient.get(      Uri.parse('https://jsonplaceholder.typicode.com/posts'))).thenAnswer(
      (_) async => response,
    );
  });

  test('Fetches all posts successfully', () async {
    final posts = await postClient.posts();

    print(posts);

    expect(posts.length, 2);
  });
}

First, we begin by creating two classes that extend Mocktail’s Mock class. One class for the HTTP Client and one class for the Response.

Afterward, we create three variables with an instance of MockHttpClient, MockResponse and PostClient. Ensure that you use the types of the actual classes when you create instances of mocked classes. Also, notice that we pass the mocked instance of our HTTP client to our PostClient instance.

After creating the variables we have to ensure that our MockHttpClient returns the right response whenever we call the posts function. Therefore we use the setUp function to ensure that all the tests inside this file have the same setup. By using the when function in combination with thenReturn, we specify the expected behavior of our mocked response. In this case, we ensure it always returns a status code of 200. As for the response body, we return a list of posts.

At last, we use the when function to return the mocked response when the get function from the mocked HTTP client is called with the https://jsonplaceholder.typicode.com/posts URL.

Adding Additional Test Cases

So far, we have been focusing on a single test case. However, we can refactor the code to ensure that it is easier to add additional tests. Furthermore, we would also like to test the post function.

Use any() to Create a General Response

In the previous example in our test file, we used the when function to return a mocked response when the posts function was called with a specific URL. However, we can simplify this approach.

import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:mock_http_clients/main.dart';
import 'package:mocktail/mocktail.dart';

class MockHttpClient extends Mock implements Client {}

class MockResponse extends Mock implements Response {}

class FakeUri extends Fake implements Uri {}

void main() {
  final Client httpClient = MockHttpClient();
  final Response response = MockResponse();
  final PostClient postClient = PostClient(httpClient: httpClient);

  setUpAll(() => registerFallbackValue(FakeUri()));

  setUp(() async {
    when(() => response.statusCode).thenReturn(200);
    when(() => response.body).thenReturn(
      jsonEncode([
        {"userId": 1, "id": 1, "title": "test", "body": "test"},
        {"userId": 1, "id": 2, "title": "test", "body": "test"},
      ]),
    );

    when(() => httpClient.get(any())).thenAnswer((_) async => response);
  });

  test('Fetches all posts successfully', () async {
    final posts = await postClient.posts();

    print(posts);

    expect(posts.length, 2);
  });
}

In the above code, we replace the URL that is called in the posts function with the any matcher. The any matcher matches any argument that is passed in. This means that every get call of the mocked HTTP client will return the same response.

To ensure that the any matcher understands the Uri type, we have to register the fallback value. First of all, we create a FakeUri class that extends the Fake class. This class implements the Uri class. After creating the class we use the setUpAll function to call the registerFallbackValue with our FakeUri class as the parameter. Afterward, the any matcher can be used inside our when function.

After making the above changes, you can run the test again and you will see that the test will succeed. Using any can make it very easy if you always want to have the same response for all the test cases in that file.

The difference between setUpAll and setUp is that setUpAll is called once before all the tests are run, while setUp is called before each individual test.

Add Test for Post Function

So far, we have been testing the posts function. However, we also want to test the post function of the PostClient class. However, if we just add the following test as shown below, the test will fail.

test('Fetches a single post successfully', () async {
  final post = await postClient.post(1);

  print(post);

  expect(post['id'], 1);
});

In the above test, we call the post function and then check if the id of the post is equal to the ID we passed into the function. However, as mentioned earlier, we recently updated the setUp function of our test to use the any matcher. As a result, calling the post function now returns a list of 2 posts, causing the test to fail.

Return Different Responses for Each Test

To ensure that our tests succeed, we can move the when function that is responsible for the response.body to the test cases themselves. So in every test, we will return a different body.

import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:mock_http_clients/main.dart';
import 'package:mocktail/mocktail.dart';

class MockHttpClient extends Mock implements Client {}

class MockResponse extends Mock implements Response {}

class FakeUri extends Fake implements Uri {}

void main() {
  final Client httpClient = MockHttpClient();
  final Response response = MockResponse();
  final PostClient postClient = PostClient(httpClient: httpClient);

  setUpAll(() => registerFallbackValue(FakeUri()));

  setUp(() async {
    when(() => response.statusCode).thenReturn(200);
    when(() => httpClient.get(any())).thenAnswer((_) async => response);
  });

  test('Fetches all posts successfully', () async {
    when(() => response.body).thenReturn(
      jsonEncode([
        {"userId": 1, "id": 1, "title": "test", "body": "test"},
        {"userId": 1, "id": 2, "title": "test", "body": "test"},
      ]),
    );

    final posts = await postClient.posts();

    print(posts);

    expect(posts.length, 2);
  });

  test('Fetches a single post successfully', () async {
    when(() => response.body).thenReturn(
      jsonEncode({"userId": 1, "id": 1, "title": "test", "body": "test"}),
    );

    final post = await postClient.post(1);

    print(post);

    expect(post['id'], 1);
  });
}

In the above code, we moved the when function that is responsible for returning the response.body to test cases themselves. You can see that we return a list of posts in the Fetches all posts successfully test and a single post in the Fetches a single post successfully test.

If you run both tests you will see that they both succeed.

Throw Exceptions With Mocked Client

So far we have always been returning data inside our tests. However, you always want to ensure that you properly catch failing HTTP calls. For this case, we can use the when function and chain it with the thenThrow function.

test('Throws an Exception when post does not exist', () async {
  when(() => httpClient.get(Uri.parse(
        'https://jsonplaceholder.typicode.com/posts/2',
      ))).thenThrow(Exception());

  expect(() => postClient.post(2), throwsA(isA<Exception>()));
});

In the above test, we throw and Exception when the post function is called with an id of 2. This is done by providing the exact URL in the when function that the mock HTTP client will receive. As you can see we are using the thenThrow function to throw an Exception.

Conclusion

Mocking HTTP clients using Mocktail is very convenient. It takes some time to get used to, but once you understand the concept, it is very easy to implement. In this post, have learned how to mock HTTP clients by calling the exact URL or any URL. You know how to modify the response and you even know how to return errors.

Tijn van den Eijnde
Tijn van den Eijnde
Articles: 38

Leave a Reply

Your email address will not be published. Required fields are marked *