Mocking HTTP in Flutter Tests: http.Client, Dio Interceptors, and MockWebServer

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 code

Make 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.4
import '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.0

Using 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.

Read more

Start now free