Mocking HTTP in Flutter Tests: http.Client, Dio Interceptors, and MockWebServer
Flutter tests that hit real HTTP endpoints are slow, flaky, and environment-dependent. The solution: inject a fake HTTP client, intercept requests at the Dio layer, or run a lightweight mock server. This guide covers all three approaches with working examples.
Key Takeaways
Inject http.Client as a dependency. Don't call http.get() directly — always inject the client so tests can swap it for a MockClient.
MockClient from package:http is the simplest approach. It takes a handler function — return any Response you want without network calls.
Dio interceptors are the clean Dio testing pattern. Add a TestInterceptor that returns canned responses before the request hits the network.
MockWebServer runs a real HTTP server on localhost. Use it when you want to test the full HTTP stack including URL construction, headers, and response parsing.
Test error paths explicitly. Mock 4xx, 5xx, and network timeouts — these paths are easy to skip and most bugs live there.
The Problem With Direct HTTP Calls
// Hard to test — http.get is not injectable
class UserApi {
Future<User> fetchUser(int id) async {
final response = await http.get(Uri.parse('https://api.example.com/users/$id'));
return User.fromJson(jsonDecode(response.body));
}
}You can't swap http.get for a fake in a test. Every test hits the network.
The fix: inject the client.
Approach 1: MockClient (package:http)
Setup
# pubspec.yaml
dev_dependencies:
http: ^1.2.0 # also in dependencies if you use it in production codeMake Your Class Testable
import 'package:http/http.dart' as http;
class UserApi {
final http.Client _client;
UserApi({http.Client? client}) : _client = client ?? http.Client();
Future<User> fetchUser(int id) async {
final response = await _client.get(
Uri.parse('https://api.example.com/users/$id'),
);
if (response.statusCode != 200) {
throw HttpException('Failed to fetch user: ${response.statusCode}');
}
return User.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}
}Write the Test
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:test/test.dart';
void main() {
group('UserApi', () {
test('fetchUser returns user on 200', () async {
final mockClient = MockClient((request) async {
expect(request.url.path, '/users/42');
return http.Response(
'{"id": 42, "name": "Alice"}',
200,
headers: {'content-type': 'application/json'},
);
});
final api = UserApi(client: mockClient);
final user = await api.fetchUser(42);
expect(user.id, 42);
expect(user.name, 'Alice');
});
test('fetchUser throws on 404', () async {
final mockClient = MockClient(
(_) async => http.Response('Not Found', 404),
);
final api = UserApi(client: mockClient);
expect(() => api.fetchUser(999), throwsA(isA<HttpException>()));
});
test('fetchUser throws on network error', () async {
final mockClient = MockClient((_) async => throw const SocketException('No connection'));
final api = UserApi(client: mockClient);
expect(() => api.fetchUser(1), throwsA(isA<SocketException>()));
});
});
}MockClient accepts a handler function — inspect the request, return any response.
Approach 2: Mockito / Mocktail
With Mocktail (no code gen)
dev_dependencies:
mocktail: ^1.0.4import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
class MockHttpClient extends Mock implements http.Client {}
void main() {
late MockHttpClient mockClient;
late UserApi api;
setUpAll(() {
registerFallbackValue(Uri());
});
setUp(() {
mockClient = MockHttpClient();
api = UserApi(client: mockClient);
});
test('calls correct URL', () async {
when(() => mockClient.get(any())).thenAnswer(
(_) async => http.Response('{"id": 1, "name": "Bob"}', 200),
);
await api.fetchUser(1);
verify(() => mockClient.get(Uri.parse('https://api.example.com/users/1'))).called(1);
});
test('throws on 500', () async {
when(() => mockClient.get(any())).thenAnswer(
(_) async => http.Response('Server Error', 500),
);
expect(() => api.fetchUser(1), throwsA(isA<HttpException>()));
});
}Mocktail shines when you need to assert on the exact URL, headers, or request body — not just the response.
Approach 3: Dio Interceptors
For apps using Dio instead of package:http:
import 'package:dio/dio.dart';
class UserApi {
final Dio _dio;
UserApi({Dio? dio}) : _dio = dio ?? Dio(BaseOptions(baseUrl: 'https://api.example.com'));
Future<User> fetchUser(int id) async {
final response = await _dio.get('/users/$id');
return User.fromJson(response.data as Map<String, dynamic>);
}
}Test Interceptor
class _MockInterceptor extends Interceptor {
final Map<String, dynamic> responses;
_MockInterceptor(this.responses);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final key = '${options.method}:${options.path}';
if (responses.containsKey(key)) {
handler.resolve(
Response(
requestOptions: options,
data: responses[key],
statusCode: 200,
),
);
} else {
handler.reject(
DioException(
requestOptions: options,
error: 'No mock for $key',
type: DioExceptionType.unknown,
),
);
}
}
}
void main() {
test('fetchUser returns user data', () async {
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
dio.interceptors.add(
_MockInterceptor({
'GET:/users/5': {'id': 5, 'name': 'Carol'},
}),
);
final api = UserApi(dio: dio);
final user = await api.fetchUser(5);
expect(user.name, 'Carol');
});
}Using Dio's Built-In HttpClientAdapter
For finer control:
class MockAdapter extends HttpClientAdapter {
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
return ResponseBody.fromString(
'{"id": 1, "name": "Test"}',
200,
headers: {
'content-type': ['application/json'],
},
);
}
@override
void close({bool force = false}) {}
}
void main() {
test('dio adapter mock', () async {
final dio = Dio()..httpClientAdapter = MockAdapter();
final api = UserApi(dio: dio);
final user = await api.fetchUser(1);
expect(user.id, 1);
});
}Approach 4: MockWebServer
For integration-level tests that run the full HTTP stack:
dev_dependencies:
mockwebserver: ^0.1.0
# or shelf for a full Dart HTTP server
shelf: ^1.4.0Using shelf as a Mock Server
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:test/test.dart';
void main() {
late HttpServer server;
late String baseUrl;
setUp(() async {
final handler = const Pipeline().addHandler((Request request) {
if (request.url.path == 'users/1') {
return Response.ok(
'{"id": 1, "name": "Alice"}',
headers: {'content-type': 'application/json'},
);
}
return Response.notFound('Not found');
});
server = await io.serve(handler, 'localhost', 0);
baseUrl = 'http://localhost:${server.port}';
});
tearDown(() => server.close());
test('fetches user from real HTTP server', () async {
final api = UserApi(baseUrl: baseUrl);
final user = await api.fetchUser(1);
expect(user.name, 'Alice');
});
test('returns null for unknown user', () async {
final api = UserApi(baseUrl: baseUrl);
expect(() => api.fetchUser(999), throwsA(isA<HttpException>()));
});
}Port 0 lets the OS pick a free port — no port conflicts between test runs.
Testing Multipart and File Uploads
test('uploadFile sends multipart request', () async {
late RequestOptions capturedRequest;
final dio = Dio();
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
capturedRequest = options;
handler.resolve(Response(requestOptions: options, statusCode: 200, data: {'url': 's3://bucket/file.png'}));
},
),
);
final api = FileApi(dio: dio);
await api.uploadFile(File('test/fixtures/sample.png'));
expect(capturedRequest.data, isA<FormData>());
final formData = capturedRequest.data as FormData;
expect(formData.files.first.key, 'file');
});Testing Auth Headers
test('includes Authorization header', () async {
late Map<String, dynamic> capturedHeaders;
final mockClient = MockClient((request) async {
capturedHeaders = request.headers;
return http.Response('{"id": 1}', 200);
});
final api = UserApi(client: mockClient, token: 'my-token');
await api.fetchUser(1);
expect(capturedHeaders['authorization'], 'Bearer my-token');
});Simulating Slow Responses
test('request times out after 5 seconds', () async {
final mockClient = MockClient((_) async {
await Future.delayed(const Duration(seconds: 10));
return http.Response('', 200);
});
final api = UserApi(client: mockClient, timeout: const Duration(seconds: 5));
expect(
() => api.fetchUser(1),
throwsA(isA<TimeoutException>()),
);
});Decision Guide
| Scenario | Best Approach |
|---|---|
| Unit testing API layer | MockClient or Mocktail |
| Asserting on request details | Mocktail verify() |
| Dio-based HTTP client | Dio interceptor |
| Full HTTP stack test | shelf MockWebServer |
| CI integration tests | MockWebServer (no network dependency) |
Keep unit tests fast with MockClient. Use MockWebServer only for integration tests where you genuinely need to validate the full request-response cycle.