How to stub target platform in Flutter

Issue

Suppose I have a widget that behaves differently according to the platform:

  • If the platform is Android, it shows a RaisedButton.
  • If the platform is iOS, it shows a CupertinoButton.

Example:

@override
Widget build(BuildContext context) {
  if (Platform.isAndroid) 
    return buildRaisedButton();
  else if (Platform.isIOS)
    return buildCupertinoButton();
  else 
    throw UnsupportedError('Only Android and iOS are supported.');
}

In my widget tests, I want to be able to test both situations, but since Platform‘s getters are static, I cannot stub them.

Any ideas on how I can achieve this?

Solution

tl;dr

Acknowledge target platform with Theme:

@override
Widget build(BuildContext context) {
  final platform = Theme.of(context).platform;

  if (platform == TargetPlatform.iOS) 
    return buildCupertinoButton();
  else 
    ...
}

Stub target platform by setting debugDefaultTargetPlatformOverride:

testWidgets('`CupertinoButton` is shown in iOS.', (tester) async {
  debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
  
  // run your tests

  debugDefaultTargetPlatformOverride = null;
});

Acknowledging target platform

In order to make your code easier to test, the target platform should be acknowledged from the Theme, not from Platform:

@override
Widget build(BuildContext context) {
  final platform = Theme.of(context).platform;

  if (platform == TargetPlatform.android) 
    return buildRaisedButton();
  else if (platform == TargetPlatform.iOS)
    return buildCupertinoButton();
  else 
    throw UnsupportedError('Only Android and iOS are supported.');
}

The getter defaultTargetPlatform should be able to cover the cases in which you don’t have access to the BuildContext.

Stubbing target platform

To stub the target platform, you must set debugDefaultTargetPlatformOverride. By default, Android is the target platform for widget tests.

Example:

testWidgets('`CupertinoButton` is shown in iOS.', (tester) async {
  debugDefaultTargetPlatformOverride = TargetPlatform.iOS;

  await tester.pumpWidget(MyWidget());

  expect(find.byType(RaisedButton), findsNothing);
  expect(find.byType(CupertinoButton), findsOneWidget);
  
  debugDefaultTargetPlatformOverride = null;
});

Notice the last line: debugDefaultTargetPlatformOverride = null.

This is necessary because, in the binding process that happens inside the function testWidgets(), the method BindingBase.initServiceExtensions() determines — based on the OS — the value of debugDefaultTargetPlatformOverride. If the operating system is not mobile (Android, iOS or Fuchsia), null gets attributed.

At the end of the test, testWidgets() calls the function debugAssertAllFoundationVarsUnset() that checks whether debugDefaultTargetPlatformOverride is null to make sure you didn’t forget to reset it to the default value. This must be done because debugDefaultTargetPlatformOverride is a top-level variable that persists across tests.

Important: You might be tempted to move debugDefaultTargetPlatformOverride = null to tearDown(), but it won’t work since debugAssertAllFoundationVarsUnset() is called before tearDown().

Answered By – Hugo Passos

Answer Checked By – Terry (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.