How to test method channel that call from native to Flutter? - flutter

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.
Here is an 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
// And there is no method like mockInvokeMethod
});
}

Snippet below based on
CyrilHu's approach, works perfect for me.
test("Listening device", () async {
ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'channnelName',
StandardMethodCodec().encodeMethodCall(
MethodCall('methodName', '{"result":true}')),(ByteData data){});
...
});

Related

How to disable admob for flutter web

i have a flutter app. It's working on web browsers and mobile devices. I have added ad mob for showing banner ads. But i cant run the project because of google_mobile_ads doesn't support web.
I'm starting google mobile ads if the current platform is a mobile device.
I have an export.dart like this:
export 'ad_mobile.dart' if (dart.library.html) 'ad_web.dart';
ad_mobile.dart:
import 'package:google_mobile_ads/google_mobile_ads.dart';
Future<dynamic> initAds() async {
await _initGoogleMobileAds();
}
Future<InitializationStatus> _initGoogleMobileAds() {
RequestConfiguration configuration = RequestConfiguration(
testDeviceIds: <String>[
testDeviceId,
],);
MobileAds.instance.updateRequestConfiguration(configuration);
return MobileAds.instance.initialize();
}
and ad_web.dart is:
import 'dart:developer';
Future initAds() async {
log('ADS DOES\'NT SUPPORTED FOR WEB PLATFORMS');
}
When i run the app on Chrome, app starts but stuck at white screen. And i get this error on debug console:
Error: MissingPluginException(No implementation found for method _init on channel plugins.flutter.io/google_mobile_ads)
There are ways to make it work with less code but if this way it is easier to maintain and extend your code.
You need to change how you import your files based on the platform.
First create an abstract class around Admob:
abstract class AdmobWrapper{
factory AdmobWrapper() => createAdmobWrapper();
Future<dynamic> init();
}
Second create the platform files:
Generic platform:
AdmobWrapper createAdmobWrapper() => throw UnimplementedError('createAdmobWrapper()');
IO (Mobile) platform:
AdmobWrapper createAdmobWrapper() => AdmobWrapperIO();
class AdmobWrapperIO() implements AdmobWrapper(){
factory AdmobWrapperIO() => _instance;
AdmobWrapperIO._internal();
static final AdmobWrapperIO_instance = AdmobWrapperIO._internal();
Future<dynamic> init(){
.... do init here
}
}
Web platform:
AdmobWrapper createAdmobWrapper() => AdmobWrapperWeb();
class AdmobWrapperWeb implements AdmobWrapper {
factory AdmobWrapperWeb() => _instance;
AdmobWrapperWeb._internal();
static final AdmobWrapperWeb _instance = AdmobWrapperWeb._internal();
Future<dynamic> init(){
// nothing here
}
}
Third add platform imports to your abstract class:
import 'package:genericPlatform.dart' // <-- this 2 slash is important
if(dart.library.io) 'package:yourRealAdmobPackage'
if(dart.library.html) 'package:yourWebPackage'
abstract class AdmobWrapper(){ /// rest of the code we made in first step
You can call AdmobWrapper().init() now.

How to mock url_launcher package for flutter test?

I used url_launcher: ^6.1.0 in my flutter project.
I start to write tests for my widgets, but the part of widgets that used the url_launcher method to launch an URL, not worked properly when running the test.
One of the methods that I used inside my Widget is like below method:
Future<void> _onTapLink(String? href) async {
if (href == null) return;
// canLaunchUrl method never return anything when we are calling this function inside flutter test
if (await canLaunchUrl(Uri.parse(href))) {
await launchUrl(Uri.parse(href));
} else {
print('cannot launch url: $href');
}
}
canLaunchUrl method never returns anything when we are calling this function inside the flutter test.
I'm looking for a way to mock the url_launcher package for using inside
flutter tests.
To mock url_launcher you may:
Add plugin_platform_interface and url_launcher_platform_interface packages to dev_dependencies section in the pubspec.yaml file.
Implement the mock class. For example, with mocktail the implementation would be:
import 'package:mocktail/mocktail.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
class MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {}
Notice that here MockPlatformInterfaceMixin mixin is used.
Configure the mock as usual. With mocktail that might be:
MockUrlLauncher setupMockUrlLauncher() {
final mock = MockUrlLauncher();
registerFallbackValue(const LaunchOptions());
when(() => mock.launchUrl(any(), any())).thenAnswer((_) async => true);
return mock;
}
Tell url_launcher to use mocked version by setting it in UrlLauncherPlatform.instance:
final mock = setupMockUrlLauncher();
UrlLauncherPlatform.instance = mock;
This is an article that explicitly explains how to mock or fake the launchUrl function. Here is an example of how to mock it with mocktail. It also uses the ioc_container package to handle substitution with dependency injection.
import 'package:fafsdfsdf/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';
class LaunchMock extends Mock {
Future<bool> call(
Uri url, {
LaunchMode? mode,
WebViewConfiguration? webViewConfiguration,
String? webOnlyWindowName,
});
}
void main() {
testWidgets('Test Url Launch', (tester) async {
//These allow default values
registerFallbackValue(LaunchMode.platformDefault);
registerFallbackValue(const WebViewConfiguration());
//Create the mock
final mock = LaunchMock();
when(() => mock(
flutterDevUri,
mode: any(named: 'mode'),
webViewConfiguration: any(named: 'webViewConfiguration'),
webOnlyWindowName: any(named: 'webOnlyWindowName'),
)).thenAnswer((_) async => true);
final builder = compose()
//Replace the launch function with a mock
..addSingletonService<LaunchUrl>(mock);
await tester.pumpWidget(
builder.toContainer()<MyApp>(),
);
//Tap the icon
await tester.tap(
find.byIcon(Icons.favorite),
);
await tester.pumpAndSettle();
verify(() => mock(flutterDevUri)).called(1);
});
}

How to mock function in flutter test

How can I mock a function in flutter and verify it has been called n times?
Ive tried implementing Mock from mockito but it only throws errors:
class MockFunction extends Mock {
call() {}
}
test("onListen is called once when first listener is registered", () {
final onListen = MockFunction();
// Throws: Bad state: No method stub was called from within `when()`. Was a real method called, or perhaps an extension method?
when(onListen()).thenReturn(null);
bloc = EntityListBloc(onListen: onListen);
// If line with when call is removed this throws:
// Used on a non-mockito object
verify(onListen()).called(1);
});
});
As a workaround I am just manually tracking the calls:
test("...", () {
int calls = 0;
bloc = EntityListBloc(onListen: () => calls++);
// ...
expect(calls, equals(1));
});
So is there a way I can create simple mock functions for flutter tests?
What you could do is this:
class Functions {
void onListen() {}
}
class MockFunctions extends Mock implements Functions {}
void main() {
test("onListen is called once when first listener is registered", () {
final functions = MockFunctions();
when(functions.onListen()).thenReturn(null);
final bloc = EntityListBloc(onListen: functions.onListen);
verify(functions.onListen()).called(1);
});
}
The accepted answer is correct, but it doesn't represent a real-life scenario where you will probably want to substitute a top-level function with a mock or a fake. This article explains how to include top-level functions in your dependency injection composition so that you can substitute those functions with mocks.
You can compose dependency injection like this and point to top-level functions such as launchUrl with ioc_container.
IocContainerBuilder compose() => IocContainerBuilder(
allowOverrides: true,
)
..addSingletonService<LaunchUrl>(
(url, {mode, webOnlyWindowName, webViewConfiguration}) async =>
launchUrl(
url,
mode: mode ?? LaunchMode.platformDefault,
webViewConfiguration:
webViewConfiguration ?? const WebViewConfiguration(),
webOnlyWindowName: webOnlyWindowName,
),
)
..add((container) => MyApp(launchUrl: container<LaunchUrl>()));
Then, you can use the technique mentioned in the answer here to mock with Mocktail.
import 'package:fafsdfsdf/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';
class LaunchMock extends Mock {
Future<bool> call(
Uri url, {
LaunchMode? mode,
WebViewConfiguration? webViewConfiguration,
String? webOnlyWindowName,
});
}
void main() {
testWidgets('Test Url Launch', (tester) async {
//These allow default values
registerFallbackValue(LaunchMode.platformDefault);
registerFallbackValue(const WebViewConfiguration());
//Create the mock
final mock = LaunchMock();
when(() => mock(
flutterDevUri,
mode: any(named: 'mode'),
webViewConfiguration: any(named: 'webViewConfiguration'),
webOnlyWindowName: any(named: 'webOnlyWindowName'),
)).thenAnswer((_) async => true);
final builder = compose()
//Replace the launch function with a mock
..addSingletonService<LaunchUrl>(mock);
await tester.pumpWidget(
builder.toContainer()<MyApp>(),
);
//Tap the icon
await tester.tap(
find.byIcon(Icons.favorite),
);
await tester.pumpAndSettle();
verify(() => mock(flutterDevUri)).called(1);
});
}

I want to execute this method in the plugin, what should I do

I am doing a function of external physical keyboard input, scan code gun to obtain data, execute this method, and then return to flutter app from the plugin
Now I have a problem, how to override this method in the plug-in, it is currently ineffective
enter image description here
Flutter provides a way to communicate with the native platform code using MethodChannel.
From the flutter MethodChannel documentation (here):
Flutter uses a flexible system that allows you to call platform-specific APIs whether available in Kotlin or Java code on Android, or in Swift or Objective-C code on iOS.
Note: If desired, method calls can also be sent in the reverse direction, with the platform acting as client to methods implemented in Dart. A concrete example of this is the [quick_actions][2] plugin.
MethodChannel usage to invoke a function on native platform-code from flutter:
We create a method channel object in Flutter, iOS, and Android with the same name. The Android and iOS objects will set a method call handler to receive calls from Flutter. The Flutter code can then call invokeMethod to call the handlers on the native objects
Flutter:
static final channelName = 'com.example.widget/keyEvent';
final methodChannel = MethodChannel(channelName);
await this.methodChannel.invokeMethod("dispatchKeyEvent");
Android:
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.example.widget/keyEvent";
#Override
public void configureFlutterEngine(#NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler(
(call, result) -> {
if (call.method.equals("dispatchKeyEvent")) {
//TODO: Call the dispatch key event function here
bool resultValue;
if (data != null) {
result.success(data);
} else {
result.error("UNAVAILABLE", "Result not available", null);
}
} else {
result.notImplemented();
}
}
);
}
}

Purpose of MethodCall list in Flutter plugin unit tests

The default unit test setup when you create a plugin looks like this:
void main() {
const MethodChannel channel = MethodChannel(
'com.example/my_plugin');
setUp(() {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return '42';
});
});
tearDown(() {
channel.setMockMethodCallHandler(null);
});
test('getPlatformVersion', () async {
expect(await MyPlugin.platformVersion, '42');
});
}
However, in a lot of the source code I see people using a List<MethodCall> called log. Here is an example:
test('setPreferredOrientations control test', () async {
final List<MethodCall> log = <MethodCall>[];
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
});
await SystemChrome.setPreferredOrientations(<DeviceOrientation>[
DeviceOrientation.portraitUp,
]);
expect(log, hasLength(1));
expect(log.single, isMethodCall(
'SystemChrome.setPreferredOrientations',
arguments: <String>['DeviceOrientation.portraitUp'],
));
});
I understand the mocking with setMockMethodCallHandler, but why use a list of MethodCall when you could just use one? If it were just one case I wouldn't may much attention, but I see the pattern over and over in the source code.
I believe the point is to verify that the method call handler was triggered exactly once (and therefore added ("logged") exactly one entry into the List<MethodCall>). If it were simply a MethodCall variable that changed from null to non-null, verifying that it was not triggered multiple times would be less straightforward.