0

Friends

I am building flutter code that is centred around API calls made with the package http

To do integration tests and widget tests I have to mock calls that use http.Client

For my testing I wish to change as little of my application code as I can. If it were up to me I would not mock the API calls, but the flutter testing framework will not allow that. If there is a way to tell the flutter testing framework to allow me access to the network that would obviate my need for this question.

The methods to do what I am trying to do that I have found (e.g: here, and here) all involve modifying my application code. I am not going to modify application code, that works, to test it. That is putting the cart before the horse.

I have found this that includes code do do what I want with the package path_provider.

Essentially

  test("Test mocking path_provider", () async {
    TestWidgetsFlutterBinding.ensureInitialized();
    const MethodChannel channelPathProvider =
        MethodChannel('plugins.flutter.io/path_provider');
    channelPathProvider.setMockMethodCallHandler((MethodCall methodCall) async {
      return "."; // <-- Breakpoint here.  This is called
    });
    Directory dir = await getApplicationDocumentsDirectory();
    print("dir: $dir");
  });

This works well, and is what I need, but for a different package.

I copied the code as faithfully as I can (making some guesses I will detail after) and it does not do what I need. I am doing something incorrect:

  test("Test mocking http", () async {
    TestWidgetsFlutterBinding.ensureInitialized();
    const MethodChannel channelPathProvider =
        MethodChannel('plugins.flutter.io/http');
    channelPathProvider.setMockMethodCallHandler((MethodCall methodCall) async {
      return Response("Body", 200); // <-- Break point here.  Not called
    });
    var client = Client();
    Uri url = Uri.parse("https://example.com/");
    Map<String, String> headers = <String, String>{
      "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
      "Accept": "application/json, text/javascript, */*; q=0.01",
    };
    var response =
        await client.post(url, headers: headers, body: '{"foo":"bar"}');
    print("response: $response Status: ${response.statusCode}");
  });
}

My guesses were two:

  • The argument to: MethodChannel('plugins.flutter.io/http'); I see this all over, but i have seen no explanation of how the argument string 'plugins.flutter.io/http' is built. What I am doing seems logical
  • The mocked method call. In the path_provider example it returns ".". The call is expecting a Directory. That takes a string as a constructor, so??? I return Result("body", 200). If the mocked function were being called, this might be a problem. But it is not so is not, yet.
Worik
  • 152
  • 1
  • 8
  • If you're using `package:http` and want to mock `http.Client`, you should 1. Make your code use a specified `http.Client` object if specified. 2. Make your tests pass an instance of the [`MockClient` class](https://pub.dev/documentation/http/latest/http.testing/MockClient-class.html). – jamesdlin Mar 31 '23 at 03:05
  • @jamesdin thatis very unhelpful. I know I can do that, I have done that, that is not the problem. – Worik Apr 02 '23 at 21:30
  • Your code shows you creating a normal `http.Client`, not a `MockClient`. Otherwise please clarify exactly what problem you are encountering. A minimal but complete example that can reproduce your problem would help others help you. – jamesdlin Apr 02 '23 at 22:17
  • janesdin: I said "For my testing I wish to change as little of my application code as I can". Is that unclear? I used the example `getApplicationDocumentsDirectory` where the communications with the package was intercepted in the test without changing code. Still unclear? – Worik Apr 03 '23 at 03:13

1 Answers1

1

Your getApplicationDocumentsDirectory example is irrelevant; package:http is not a Flutter plugin and does not use method channels.

If you care only about Dart VM builds (i.e., not Dart for the Web), then package:http is built upon dart:io's HttpClient, which can be overridden via HttpOverrides.

However, manually using HttpOverrides is a lot of work. The third-party fake_http_client package does it for you:

import 'dart:io' as io;
import 'package:fake_http_client/fake_http_client.dart';
import 'package:http/http.dart' as http;

Future<void> doHttpWork() async {
  var httpClient = http.Client();
  var response = await httpClient.get(Uri.parse('https://www.example.com/'));
  if (response.statusCode != 200) {
    print('Failed: ${response.statusCode}');
    return;
  }

  print(response.body);
}

void main() async {
  await doHttpWork(); // Prints normal content.

  await io.HttpOverrides.runZoned(
    () async {
      await doHttpWork(); // Prints fake content.
    },
    createHttpClient: (_) => FakeHttpClient(
      (request, client) {
        if (request.uri == Uri.parse('https://www.example.com/')) {
          return FakeHttpResponse(
            statusCode: 200,
            body: 'hello world!',
          );
        }
        return FakeHttpResponse();
      },
    ),
  );
}

If you care about Dart for the Web, then HttpOverrides won't work. In general, most people using package:http should be using its MockClient class. Using it might involve making some changes to existing code, but it usually can be very minor and non-intrusive. For example, you could modify the example above to be:

import 'package:http/http.dart' as http;
import 'package:http/testing.dart' as http;

http.Client theHttpClient = http.Client();

Future<void> doHttpWork() async {
  var response = await theHttpClient.get(Uri.parse('https://www.example.com/'));
  if (response.statusCode != 200) {
    print('Failed: ${response.statusCode}');
    return;
  }

  print(response.body);
}

void main() async {
  theHttpClient = http.MockClient((request) async {
    if (request.url == Uri.parse('https://www.example.com/')) {
      return http.Response('hello world!', 200);
    }
    return http.Response('', 404);
  });

  await doHttpWork(); // Prints fake content.
}
jamesdlin
  • 81,374
  • 13
  • 159
  • 204
  • "Your getApplicationDocumentsDirectory example is irrelevant; package:http is not a Flutter plugin and does not use method channels." That is helpful. Clearly I was confused. That will explain why the method did not work. – Worik Apr 03 '23 at 23:32