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
andsetUp
is thatsetUpAll
is called once before all the tests are run, whilesetUp
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.