'Mock a Widget in Flutter tests

I am trying to create tests for my Flutter application. Simple example:

class MyWidget extends StatelessWidget {
   @override
   build(BuildContext context) {
      return MySecondWidget();
   }
}

I would like to verify that MyWidget is actually calling MySecondWidget without building MySecondWidget.

void main() {
   testWidgets('It should call MySecondWidget', (WidgetTester tester) async {
      await tester.pumpWidget(MyWidget());
      expect(find.byType(MySecondWidget), findsOneWidget);
   }
}

In my case this will not work because MySecondWidget needs some specific and complex setup (like an API key, a value in a Provider...). What I would like is to "mock" MySecondWidget to be an empty Container (for example) so it doesn't raise any error during the test.

How can I do something like that ?



Solution 1:[1]

There is nothing done out of the box to mock a widget. I'm going to write some examples/ideas on how to "mock"/replace a widget during a test (for example with a SizedBox.shrink().

But first, let me explain why I think this is not a good idea.

In Flutter you are building a widget tree. A specific widget has a parent and usually has one or several children.

Flutter chose a single pass layout algorithm for performance reasons (see this):

Flutter performs one layout per frame, and the layout algorithm works in a single pass. Constraints are passed down the tree by parent objects calling the layout method on each of their children. The children recursively perform their own layout and then return geometry up the tree by returning from their layout method. Importantly, once a render object has returned from its layout method, that render object will not be visited again until the layout for the next frame. This approach combines what might otherwise be separate measure and layout passes into a single pass and, as a result, each render object is visited at most twice during layout: once on the way down the tree, and once on the way up the tree.

From this, we need to understand that a parent needs its children to build to get their sizes and then render itself properly. If you remove its children, it might behave completely differently.

It is better to mock the services if possible. For example, if your child makes an HTTP request, you can mock the HTTP client:

HttpOverrides.runZoned(() {
  // Operations will use MyHttpClient instead of the real HttpClient
  // implementation whenever HttpClient is used.
}, createHttpClient: (SecurityContext? c) => MyHttpClient(c));

If the child needs a specific provider you can provide a dummy one:

testWidgets('My test', (tester) async {
  tester.pumpWidget(
    Provider<MyProvider>(
      create: (_) => MyDummyProvider(),
      child: MyWidget(),
    ),
  );
});

If you still want to change a widget with another one during your tests, here are some ideas:

1. Use Platform.environment.containsKey('FLUTTER_TEST')

You can either import Platform from dart:io (not supported on web) or universal_io (supported on web).

and your build method could be:

@override
Widget build(BuildContext context) {
  final isTest = Platform.environment.containsKey('FLUTTER_TEST');
  if (isTest) return const SizedBox.shrink();
  return // Your real implementation.
}

2. Use the annotation @visibleForTesting

You can annotate a parameter (ex: mockChild) that is only visible/usable in a test file:

class MyWidget extends StatelessWidget {
  const MyWidget({
    @visibleForTesting this.mockChild,
  });
  
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    return mockChild ??  // Your real widget implementation here.
  }
}

And in your test:

tester.pumpWidget(
  MyWidget(
    mockChild: MyMockChild(),
  ),
);

Solution 2:[2]

You can mock MySecondWidget (eg using Mockito) but you do need to change your real code to create a MockMySecondWidget when in test mode, so it's not pretty. Flutter does not support object instantiation based on a Type (except through dart:mirrors but that is not compatible with Flutter), so you cannot 'inject' the type as a dependency. To determine if you are in test mode use Platform.environment.containsKey('FLUTTER_TEST') - best to determine this once upon startup and set the result as a global final variable, which will make any conditional statements quick.

Solution 3:[3]

One way to do it, is to wrap the child widget into a function, and pass the function to parent widget's constructor:

class MyWidget extends StatelessWidget {
  final Widget Function() buildMySecondWidgetFn;

  const MyWidget({
    Key? key,
    this.buildMySecondWidgetFn = _buildMySecondWidget
  }): super(key: key);

  @override
  build(BuildContext context) {
    return buildMySecondWidgetFn();
  }
}

Widget _buildMySecondWidget() => MySecondWidget();

Then you can make up your mock widget, pass it thru buildMySecondWidgetFn in test.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Valentin Vignal
Solution 2 Bram
Solution 3 Dennis Cheng