With Dart's package:http, how do you test Flutter code that makes HTTP requests without sending anything over the network?
answer
- inject the Client
- package:http/testing.dart
- handler returns a Response
- inspect request.url
- test binding answers 400
basics
~10 sInject an http.Client into the class that makes requests, and in tests pass MockClient from package:http/testing.dart, whose handler receives each Request and returns an http.Response, such as Response(body, 200).
solid answer
~30 sThe code under test must take an `http.Client` instead of calling top-level `http.get`, so a test can substitute one. `package:http/testing.dart` provides `MockClient`, a real `Client` whose constructor takes a handler: `MockClient((request) async => Response(jsonBody, 200))`. The handler can inspect `request.url`, `request.method` and headers and return different responses, including error status codes, which tests the parsing and error mapping in `WeatherApi` without a network. `MockClient.streaming` exists for streamed responses. Under `TestWidgetsFlutterBinding`, an un-injected `dart:io` `HttpClient` returns empty 400 responses instead of reaching the network.
code
dart · 17 linesimport 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
void main() {
test('fetchForecast sends the city and parses the body', () async {
final client = MockClient((request) async {
expect(request.method, 'GET');
expect(request.url.queryParameters['city'], 'Oslo');
return http.Response('{"highC":14,"lowC":6}', 200);
});
final forecast = await WeatherApi(client).fetchForecast('Oslo');
expect(forecast.highC, 14);
});
}go deeper
Recall the two parts: inject an http.Client into the API class, then pass MockClient from package:http/testing.dart with a handler returning Response objects.
Explain what the handler can assert on (URL, method, headers) and how to cover status-code and connection-failure branches separately.
Place the seam correctly: MockClient for the API class, a fake or mock of the API for view models, and recognise the test binding's 400 responses in failures.
Decide where HTTP is tested across layers and whether contract checks against a real server supplement MockClient-based tests.
## The problem A `WeatherApi` class that calls a forecast endpoint and turns the JSON into a `Forecast` has logic worth testing: the URL it builds, how it parses the body, and what it does with a 404 or a 500. Sending real requests in a unit test is slow, flaky and depends on a server you do not control. The `http` package (version 1.x, the Dart team's HTTP client package) ships its own test double for exactly this. ## Step 1: make the client injectable The top-level functions `http.get` and `http.post` create a client internally, so a test cannot replace it. The class must receive a `Client`: ```dart class WeatherApi { WeatherApi(this._client); final http.Client _client; Future<Forecast> fetchForecast(String city) async { final res = await _client.get(Uri.https('api.example.com', '/forecast', {'city': city})); if (res.statusCode != 200) throw WeatherApiException(res.statusCode); return Forecast.fromJson(jsonDecode(res.body) as Map<String, dynamic>); } } ``` Production code passes `http.Client()`; tests pass a `MockClient`. ## Step 2: `MockClient` `MockClient` lives in `package:http/testing.dart`. It extends `BaseClient`, so it **is** a real `Client` — nothing to stub with `when`. Its constructor takes a **handler**: - `MockClient((Request request) async => Response(body, statusCode))` — the handler receives each fully built `Request` and returns a `Response`. - `MockClient.streaming((request, bodyStream) async => StreamedResponse(...))` — for tests of streamed bodies. Inside the handler you can: 1. check `request.url.path` and `request.url.queryParameters` to assert the URL is right; 2. check `request.method` and `request.headers`, such as an auth header; 3. return different bodies or status codes per path; 4. throw a `ClientException` to simulate a network failure. ## Step 3: write the tests ```dart test('parses a 200 response', () async { final client = MockClient((request) async { expect(request.url.queryParameters['city'], 'Oslo'); return http.Response('{"highC":14,"lowC":6}', 200); }); final forecast = await WeatherApi(client).fetchForecast('Oslo'); expect(forecast.highC, 14); }); test('maps a 404 to WeatherApiException', () async { final client = MockClient((_) async => http.Response('', 404)); expect(WeatherApi(client).fetchForecast('Nowhere'), throwsA(isA<WeatherApiException>())); }); ``` ## MockClient versus a mocking library | Approach | What you write | What it checks | |---|---|---| | `MockClient` | a handler function returning `Response` | the full request the client would send | | mockito or mocktail mock of `http.Client` | a stub per method (`get`, `post`) | only the call you stubbed | | a fake `WeatherApi` | an in-memory class | nothing about HTTP; used by view-model tests | The Flutter cookbook shows the mockito route, with `@GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)])`, renaming the mock precisely because `package:http/testing.dart` already exports a class called `MockClient`. `MockClient` needs no code generation and sees the complete `Request` whichever method was used, so it is usually the simpler choice for testing the `WeatherApi` layer. Above that layer, view-model tests should replace `WeatherApi` itself rather than HTTP. ## What the Flutter test binding does to real HTTP When tests run under `TestWidgetsFlutterBinding` — every `testWidgets` test — `flutter_test` sets `HttpOverrides.global` so any `dart:io` `HttpClient` created without an override returns **empty 400 responses** and makes no network request, printing a warning if the test fails. A test that forgot to inject a client therefore sees a 400 status, not a timeout or a real response. That is a clue in a failing test, and another reason to inject the client explicitly. ## Common mistakes - Calling top-level `http.get` inside the class, leaving nothing to inject. - Testing only the happy path; the status-code and exception branches are where parsing code usually breaks. - Mocking `http.Client` in view-model tests instead of mocking the `WeatherApi` the view model actually uses. - Forgetting that a handler which never returns for a path — for example one with a missing `case` that awaits forever — makes the test hang until its timeout instead of failing clearly; return a 404 by default.
- How do you simulate a dropped connection rather than an error status with MockClient?Throw from the handler — for example `throw http.ClientException('connection reset')` — instead of returning a `Response`. The `Future` returned by `client.get` then completes with that error, which exercises the code's network-failure branch rather than its status-code branch.
- Why does the Flutter cookbook rename its mockito mock of http.Client to MockHttpClient?`package:http/testing.dart` already exports a class named `MockClient`. Generating a mockito mock with the default name for `http.Client` would collide with it, so the cookbook uses `MockSpec<http.Client>(as: #MockHttpClient)` to give the generated class a distinct name.
saying these in an interview costs you the question
- Calls top-level http.get inside the class, so nothing can be injected
- Thinks MockClient needs build_runner or a when() stub per method
- Expects testWidgets to reach the real network with dart:io HttpClient
- Mocks http.Client in view-model tests instead of the API class
- Tests only the 200 path and never the error status codes