If you’re not familiar with HTTP and Flutter widget
tests, testWidgets()
mocks
the
Dart http.Client
to always return
an
HTTP 400 status!
Surprising, I know. The idea is that you shouldn’t make real HTTP calls from your widget tests.
But I think 99.9% of the time, this is not the response you want.
Typically, you could
mock the HTTP client
with
MockClient.
This will work if you can provide an HttpClient
directly to your widget. Or, if you have a client-side
service, you could just mock the whole service. But I recently had a
case where I needed to mock the HTTP client which was used two packages deep in my dependencies.
Because of the interdependencies, I had no choice but to mock the HTTP client. I spent a couple
of hours trying to figure out how to do this. Even after I figured out the strategy, it took another
45 minutes to create the correct mocking incantation. If you’re in this situation, let me save
you some time.
The first thing I discovered
was
HttpOverrides.
Go ahead and search the https://dart.dev or https://flutter.dev sites now for “HttpOverrides”.
I’ll save you some time - you’ll find a single changelog entry. HttpOverrides
allows you to mock the Dart HTTP client.
I then used
mockito to create a mock HttpClient
.
Additionally, I had to mock methods of HttpClient
, HttpClientRequest
, HttpClientResponse
,
and HttpHeaders
.
Here’s the code:
my_test.dart
:
...
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
...
@GenerateMocks([
HttpClient,
HttpClientRequest,
HttpClientResponse,
HttpHeaders,
])
import 'my_test.mocks.dart';
...
final mockHttpClient = MockHttpClient();
...
class MockHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return mockHttpClient;
}
}
void main() {
setUp(() { // ... or setUpAll() ...
HttpOverrides.global = MockHttpOverrides();
when(mockHttpClient.openUrl(any, any)).thenAnswer((invocation) {
final url = invocation.positionalArguments[1] as Uri;
// ... customize your response based on the URL, or method in invocation.positionalArguments[0]
final body = url.path.contains('/details/')
? responseBody
: aDifferentResponseBody;
final request = MockHttpClientRequest();
final response = MockHttpClientResponse();
when(request.close()).thenAnswer((_) => Future.value(response));
when(request.addStream(any)).thenAnswer((_) async => null);
when(response.headers).thenReturn(MockHttpHeaders());
when(response.handleError(any, test: anyNamed('test')))
.thenAnswer((_) => Stream.value(body));
when(response.statusCode).thenReturn(200);
when(response.reasonPhrase).thenReturn('OK');
when(response.contentLength).thenReturn(body.length);
when(response.isRedirect).thenReturn(false);
when(response.persistentConnection).thenReturn(false);
return Future.value(request);
});
});
testWidgets('works properly', (tester) async {
... test your widget which make an HTTP request...
});
}
final responseBody = utf8.encode(jsonEncode({
"your_json_response": "goes here",
...
}));
In my case, I was mocking an HTTP GET
request, so YMMV if you’re making a different request.
Also, you can use HttpOverrides.runZoned()
if you don’t want to set HttpOverrides.global
for
every request.
Enjoy!