Mocking Dart HTTP Client When Testing Flutter Widgets

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!


See also

comments powered by Disqus