14

I read the introduction to platform-specific plugins/channels on the Flutter website and I browsed some simple examples of a plugin, like url_launcher:

// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/services.dart';

const _channel = const MethodChannel('plugins.flutter.io/url_launcher');

/// Parses the specified URL string and delegates handling of it to the
/// underlying platform.
///
/// The returned future completes with a [PlatformException] on invalid URLs and
/// schemes which cannot be handled, that is when [canLaunch] would complete
/// with false.
Future<Null> launch(String urlString) {
  return _channel.invokeMethod(
    'launch',
    urlString,
  );
}

In widgets tests or integration tests, how can I mock out or stub channels so I don't have to rely on the real device (running Android or iOS) say, actually launching a URL?

matanlurey
  • 8,096
  • 3
  • 38
  • 46

3 Answers3

20

MethodChannel#setMockMethodCallHandler is deprecated and removed as of now.

Looks like this is the way to go now:

import 'package:flutter/services.dart'; 
import 'package:flutter_test/flutter_test.dart';

void mockUrlLauncher() {
  const channel = MethodChannel('plugins.flutter.io/url_launcher');

  handler(MethodCall methodCall) async {
    if (methodCall.method == 'yourMethod') {
      return 42;
    }
    return null;
  }

  TestWidgetsFlutterBinding.ensureInitialized();

  TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
      .setMockMethodCallHandler(channel, handler);
}

The details are on GitHub.

And here is a tested example for package_info plugin for future references:

import 'package:flutter/services.dart'; 
import 'package:flutter_test/flutter_test.dart';

void mockPackageInfo() {
  const channel = MethodChannel('plugins.flutter.io/package_info');

  handler(MethodCall methodCall) async {
    if (methodCall.method == 'getAll') {
      return <String, dynamic>{
        'appName': 'myapp',
        'packageName': 'com.mycompany.myapp',
        'version': '0.0.1',
        'buildNumber': '1'
      };
    }
    return null;
  }

  TestWidgetsFlutterBinding.ensureInitialized();

  TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
      .setMockMethodCallHandler(channel, handler);
}
jibiel
  • 8,175
  • 7
  • 51
  • 74
  • This should be accepted answer since Flutter 2.5, more info https://flutter.dev/docs/release/breaking-changes/mock-platform-channels – PoQ Sep 17 '21 at 23:36
  • 1
    `TestDefaultBinaryMessengerBinding.instance` in my test is nullable so i should add null check ('?' or '!') – Fuad Reza Apr 08 '22 at 03:43
  • How to mock PlatformException in methodchannel? I want to test the return value in my catch block. – sagar suri Jan 25 '23 at 17:47
  • I cannot find how the "magic incantation" `plugins.flutter.io` was derived: `const channel = MethodChannel('plugins.flutter.io/package_info');` – Worik Mar 30 '23 at 22:37
14

You can use setMockMethodCallHandler to register a mock handler for the underlying method channel:

https://docs.flutter.io/flutter/services/MethodChannel/setMockMethodCallHandler.html

final List<MethodCall> log = <MethodCall>[];

MethodChannel channel = const MethodChannel('plugins.flutter.io/url_launcher');

// Register the mock handler.
channel.setMockMethodCallHandler((MethodCall methodCall) async {
  log.add(methodCall);
});

await launch("http://example.com/");

expect(log, equals(<MethodCall>[new MethodCall('launch', "http://example.com/")]));

// Unregister the mock handler.
channel.setMockMethodCallHandler(null);
Adam Barth
  • 189
  • 1
  • 2
  • Just so readers understand, because `MethodChannel` is a canonical (`const`) object, I can create and `setMockMethodCallHandler` in my own package even if the plugin author did not make `channel` public, right? – matanlurey May 10 '17 at 16:17
  • 1
    The plugin needs to provide an @visibleForTesting constructor that takes a MethodChannel argument as an alternative to using the default one. The url_launcher doesn't do this yet, but the firebase_analytics plugin is an example of this patterns. Eventually I'm hoping that most plugins will follow this pattern and will be more testable. https://github.com/flutter/firebase_analytics/blob/master/lib/firebase_analytics.dart – Collin Jackson May 10 '17 at 22:46
  • @CollinJackson, Do you have an update on that link? Or a guide? The old link is dead. – Suragch Jun 25 '19 at 16:58
  • 1
    Here's the fixed link https://github.com/flutter/plugins/blob/master/packages/firebase_analytics/lib/firebase_analytics.dart Here's another more recently updated plugin that is a bit cleaner: https://github.com/flutter/plugins/blob/master/packages/cloud_functions/lib/src/cloud_functions.dart – Collin Jackson Jul 03 '19 at 02:37
  • @CollinJackson all of those links are dead, can you provide a permanent gist which will never go down instead? – Mark O'Sullivan Dec 13 '22 at 11:51
  • How to mock PlatformException in methodchannel? I want to test the return value in my catch block. – sagar suri Jan 25 '23 at 17:48
8

When you create a plugin, you are automatically provided a default test:

void main() {
  const MethodChannel channel = MethodChannel('my_plugin');

  setUp(() {
    channel.setMockMethodCallHandler((MethodCall methodCall) async {
      return '42';
    });
  });

  tearDown(() {
    channel.setMockMethodCallHandler(null);
  });

  test('getPlatformVersion', () async {
    expect(await MyPlugin.platformVersion, '42');
  });
}

Let me add some notes about it:

  • Calling setMockMethodCallHandler allows you to bypass whatever the actual plugin does and return your own value.
  • You can differentiate methods using methodCall.method, which is a string of the called method name.
  • For plugin creators this is a way to verify the public API names, but it does not test the functionality of the API. You need to use integration tests for that.
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393