flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
166.18k stars 27.45k forks source link

Simulate invokeMethod in MethodChannel from native to Flutter #63465

Open benzsuankularb opened 4 years ago

benzsuankularb commented 4 years ago

I'm developing a native plugin and trying to do unit tests. All unit tests will be done in Dart (No native code).

Flutter has a test example of how you can test call method channel from Dart to native using setMockMethodCallHandler.

The problem is I've not found the way to test method channel that calls from native to Dart that uses setMethodCallHandler to handle a call from native.

For example,

// main.dart

class Plugin {

  static MethodChannel _channel = const MethodChannel('plugin');

  Plugin() {
    _channel.setMethodCallHandler((call) async {
      print("called from native: ${call.method}");
    });
  }

}
// tests/main_test.dart

void main() {
  const MethodChannel _channel = MethodChannel('core.super_router');
  Plugin plugin;

  setUp(() async {
    TestWidgetsFlutterBinding.ensureInitialized();
    plugin = Plugin();
  });

  test("call from native", () async {
    _channel.invokeMethod("something");
    // This call can't reach the handler in the Plugin
  });
}

I'm purpose for method like so _channel.simulateInvokeMethod("something");

garry-jeromson commented 3 years ago

Was facing the same issue, and ended up being able to implement a solution very similar to the one used here: https://github.com/flutter/plugins/blob/4290d18f0e43288087b3754c41d4739872d9a152/packages/webview_flutter/test/webview_flutter_test.dart.

The _FakePlatformViewsController and FakePlatformWebView classes are where the magic happens; several of the methods on FakePlatformWebView use ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage to simulate the native-to-flutter method invocation.

garry-jeromson commented 3 years ago

For your example, it'd be something like this (roughly):

// tests/main_test.dart

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  setUpAll(() {
SystemChannels.platform_views.setMockMethodCallHandler(fakePlatformViewsController.fakePlatformViewsMethodHandler);
  });

  setUp(() {
    fakePlatformViewsController.reset();
  });

  testWidgets("call from native", (tester) async {
    tester.pumpWidget(PluginView());
    final pluginView = fakePlatformViewsController.lastCreatedView;
    await pluginView.fakeSomethingCall("hello");
    // Make some assertions here
  });
}

class _FakePlatformViewsController {
  FakePluginView lastCreatedView;

  Future<dynamic> fakePlatformViewsMethodHandler(MethodCall call) {
    switch (call.method) {
      case 'create':
        final args = call.arguments as Map<Object, Object>;
        lastCreatedView = FakePluginView(
          args['id'] as int,
        );
        return Future<int>.sync(() => 1);
      default:
        return Future<void>.sync(() {});
    }
  }

  void reset() {
    lastCreatedView = null;
  }
}

class FakePluginView {
  FakePluginView(int id) {
    channel = MethodChannel('core.super_router_$id');
    channel.setMockMethodCallHandler(onMethodCall);
  }

  MethodChannel channel;

  Future<Object> onMethodCall(MethodCall call) {
    switch (call.method) {
      case "something":
        return Future<void>.sync(() {});
      default:
        return Future<void>.sync(() {});
    }
  }

  Future<void> fakeSomethingCall(String someArgumentValue) {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall('something', {"someArgumentName": someArgument}));

    return ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
      channel.name,
      data,
      (ByteData data) {},
    );
  }
}
garry-jeromson commented 3 years ago

Still very much supportive of the feature request, though - the above approach is a lot of faff and it'd be much nicer to have a simple API for testing these kind of method invocations.

CyrilHu commented 3 years ago

from @garry-jeromson answer, add extension below

extension MethodChannelMock on MethodChannel {
  Future<void> invokeMockMethod(String method, dynamic arguments) async {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall(method, arguments));

    return ServicesBinding.instance?.defaultBinaryMessenger
        .handlePlatformMessage(
      name,
      data,
      (ByteData? data) {},
    );
  }
}

then just use invokeMockMethod instead of invokeMethod

test("call from native", () async {
  _channel.invokeMockMethod("something");
});
AlvaroMenezes commented 3 years ago

@CyrilHu answer works perfect for me!

jm-marquez-humanitcare commented 10 months ago
extension MethodChannelMock on MethodChannel {
  Future<void> invokeMockMethod(String method, dynamic arguments) async {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall(method, arguments));

    return ServicesBinding.instance?.defaultBinaryMessenger
        .handlePlatformMessage(
      name,
      data,
      (ByteData? data) {},
    );
  }
}

Revised version with new class:

extension MethodChannelMock on MethodChannel {
  Future<void> invokeMockMethod(String method, [dynamic arguments]) async {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall(method, arguments));
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(name, data, (data) {});
  }
}

Thanks @CyrilHu.

SlavikHaltia commented 3 months ago
extension MethodChannelMock on MethodChannel {
  Future<void> invokeMockMethod(String method, dynamic arguments) async {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall(method, arguments));

    return ServicesBinding.instance?.defaultBinaryMessenger
        .handlePlatformMessage(
      name,
      data,
      (ByteData? data) {},
    );
  }
}

Revised version with new class:

extension MethodChannelMock on MethodChannel {
  Future<void> invokeMockMethod(String method, [dynamic arguments]) async {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall(method, arguments));
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(name, data, (data) {});
  }
}

Thanks @CyrilHu.

@jm-marquez-humanitcare the revised version loses track of the asynchronous events, i guess it would be better to 'await' the last line:

extension MethodChannelMock on MethodChannel {
  Future<void> invokeMockMethod(String method, [dynamic arguments]) async {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall(method, arguments));
    await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(name, data, (data) {});
  }
}
adamrhunter commented 3 months ago
extension MethodChannelMock on MethodChannel {
  Future<void> invokeMockMethod(String method, dynamic arguments) async {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall(method, arguments));

    return ServicesBinding.instance?.defaultBinaryMessenger
        .handlePlatformMessage(
      name,
      data,
      (ByteData? data) {},
    );
  }
}

Revised version with new class:

extension MethodChannelMock on MethodChannel {
  Future<void> invokeMockMethod(String method, [dynamic arguments]) async {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall(method, arguments));
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(name, data, (data) {});
  }
}

Thanks @CyrilHu.

@jm-marquez-humanitcare the revised version loses track of the asynchronous events, i guess it would be better to 'await' the last line:

extension MethodChannelMock on MethodChannel {
  Future<void> invokeMockMethod(String method, [dynamic arguments]) async {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall(method, arguments));
    await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(name, data, (data) {});
  }
}

@SlavikHaltia - or, allow await control from the caller:

extension MethodChannelMock on MethodChannel {
  Future<ByteData?> invokeMockMethod(String method, [dynamic arguments]) async {
    const codec = StandardMethodCodec();
    final data = codec.encodeMethodCall(MethodCall(method, arguments));
    return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(name, data, (ByteData? data) {});
  }
}