我该如何模拟/存根Flutter平台通道/插件?

14

我阅读了Flutter网站上有关平台特定插件/通道的介绍,并浏览了一些插件的简单示例,比如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,
  );
}
在小部件测试或集成测试中,我该如何模拟或替换通道,以便不必依赖真实设备(运行Android或iOS),比如实际启动URL?
4个回答

21

MethodChannel#setMockMethodCallHandler现已被废弃并删除。

现在好像应该使用以下方法:

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);
}

具体细节请查看GitHub

以下是package_info插件的已测试示例,供日后参考:

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);
}

自Flutter 2.5起,这应该是被接受的答案,更多信息请参见https://flutter.dev/docs/release/breaking-changes/mock-platform-channels。 - PoQ
1
在我的测试中,TestDefaultBinaryMessengerBinding.instance 是可空的,因此我应该添加空值检查('?' 或 '!')。 - Fuad Reza
如何在MethodChannel中模拟PlatformException?我想测试我的catch块中的返回值。 - sagar suri
我找不到“魔法咒语” plugins.flutter.io 是如何衍生出来的:const channel = MethodChannel('plugins.flutter.io/package_info'); - Worik

15
你可以使用setMockMethodCallHandler为底层方法通道注册一个模拟处理程序:

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.
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
          .setMockMethodCallHandler(channel, (message) {
  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);

只是为了让读者理解,因为MethodChannel是一个规范(const)对象,即使插件作者没有将channel公开,我也可以在自己的包中创建并设置setMockMethodCallHandler,对吗? - matanlurey
1
插件需要提供一个@visibleForTesting构造函数,该构造函数接受一个MethodChannel参数作为使用默认构造函数的替代方案。目前url_launcher还没有这样做,但firebase_analytics插件是这种模式的一个例子。最终,我希望大多数插件都会遵循这种模式,并且更容易进行测试。https://github.com/flutter/firebase_analytics/blob/master/lib/firebase_analytics.dart - Collin Jackson
@CollinJackson,你有这个链接的更新吗?或者有指南吗?旧链接已经失效了。 - Suragch
1
这是修复后的链接 https://github.com/flutter/plugins/blob/master/packages/firebase_analytics/lib/firebase_analytics.dart这是另一个更近期更新的插件,代码更加简洁: https://github.com/flutter/plugins/blob/master/packages/cloud_functions/lib/src/cloud_functions.dart - Collin Jackson
如何在MethodChannel中模拟PlatformException?我想测试我的catch块中的返回值。 - sagar suri
显示剩余3条评论

8

当您创建插件时,系统会自动为您提供一个默认测试:

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');
  });
}

让我补充一些关于它的说明:

  • 调用setMockMethodCallHandler可以绕过插件实际执行的操作并返回您自己的值。
  • 您可以使用methodCall.method来区分方法,它是被调用方法名称的字符串。
  • 对于插件创建者来说,这是验证公共API名称的一种方式,但它并不能测试API的功能性。您需要使用集成测试来进行测试。

如何在MethodChannel中模拟PlatformException?我想测试我的catch块中的返回值。 - sagar suri
@sagarsuri,抱歉,我现在不确定。您能否以您期望的其他异常类型相同的方式执行此操作? - Suragch
我尝试了,但异常没有被模拟,我无法进行测试。 - sagar suri
@sagarsuri,如果您将代码发布为新答案并从此处链接回来,我会查看它。 - Suragch

0
在我的情况下,我的频道是私人的,所以我无法访问它。所以一般来说,我的情况是这样的:
 testWidgets('My test', (WidgetTester tester) async {
    //https://github.com/flutter/flutter/issues/76790 - add delay work around
    await tester.runAsync(() async {
      //arrange
      MethodChannel channel = const MethodChannel('my_channel');
      WidgetsFlutterBinding.ensureInitialized();
        tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (message) => null);
      await Future.delayed(Duration(milliseconds: 400));
    });
  });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接