How to `setUp` a `WidgetTester` for Multiple Tests in Flutter

3,718

Below is what looks like the current way to solve for this in Flutter.

To summarize:

  1. Create the group(..) structure inside main()
  2. Create your own private methods from inside that structure, for each group of testing you want. For each of these private methods:
    • Pass in the WidgetTester instance
    • Let them be async
  3. And then you should only have a single call to testWidgets(..)
    • Inside this method, is where you call the private methods you set up to distribute test logic
    • Call each of these with await, so they don't run concurrently

So far I didn't find a way for the output to indicate each "sub-test" it ran, so just using print(...) statements for now.

This is a demo for some QR Code logic:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:qr_code_demo/app/appRoutes.dart';
import 'package:qr_code_demo/view/appHome.dart';
import 'package:qr_code_demo/view/qrScanner.dart';

class MockNavigatorObserver extends Mock implements NavigatorObserver {}

void main() {
  group('MainPage navigation tests', () {
    NavigatorObserver mockObserver;

    _loadAppHomeScreen(WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          routes: AppRoutes.getRouteMap(),
          home: AppHomeScreen(),
          navigatorObservers: [mockObserver],
        ),
      );
    }

    setUp(() {
      mockObserver = MockNavigatorObserver();
    });

    Future<Null> _verifyLayoutElements(WidgetTester tester) async {
      print('_verifyLayoutElements');
      expect(find.byIcon(Icons.scanner), findsOneWidget);
      expect(find.byType(FloatingActionButton), findsOneWidget);
      expect(find.byType(RaisedButton), findsOneWidget);
    }

    Future<Null> _navigateToQrScannerScreen(WidgetTester tester) async {
      print('_navigateToQrScannerScreen');

      await tester.tap(find.byIcon(Icons.scanner));
      await tester.pumpAndSettle();

      verify(mockObserver.didPush(any, any));

      expect(find.byType(AppHomeScreen), findsNothing);
      expect(find.byType(QrScannerScreen), findsOneWidget);
    }

    testWidgets('AppHomeScreen WidgetTester', (WidgetTester tester) async {
      await _loadAppHomeScreen(tester);

      await _verifyLayoutElements(tester);
      await _navigateToQrScannerScreen(tester);
    });
  });
}

Thanks to: https://iiro.dev/2018/08/22/writing-widget-tests-for-navigation-events/

  • Scroll to code for this file: test/navigation_test.dart

====

And double-thanks, because the navigation testing logic including with this example is thanks to @iiro's post: https://stackoverflow.com/a/51983194/2162226

Here is the appRoutes.dart file:

import 'package:qr_code_demo/view/appHome.dart';
import 'package:qr_code_demo/view/qrScanner.dart';  

class AppRoutes {

  static const String AppHome = 'AppHome';
  static const String QrScanner = 'QrScanner';

  static String initialRoute() {
    return AppHome;
  }
  
  static getRouteMap() {
    
    return {
      AppRoutes.AppHome: (context) => AppHomeScreen(),
      AppRoutes.QrScanner: (context) => QrScannerScreen()
    };
    
  }
}
Share:
3,718
Philippe Fanaro
Author by

Philippe Fanaro

Graduated as an Electrical/Telecommunications Engineer in 2017, from the University of São Paulo, Brazil. Converted to Machine Learning in 2018. Became a Flutter App Developer in 2019. I'm also a ("retired") Go (Baduk or Weiqi) player, and I also have a website with some curious and interesting stuff: fanaro.io

Updated on December 18, 2022

Comments

  • Philippe Fanaro
    Philippe Fanaro over 1 year

    1. The Problem

    The testWidgets function is apparently only a subcase of the test function.

    A use case I'm trying to solve right now is to pump the same widget for multiple testWidgets, a setUp for multiple testWidgets. However, how can I do this if it creates a new instance inside each test?

    I've tried to initialize a WidgetTester outside the tests, in the main(), but WidgetTester has only a private constructor:

    class WidgetTester 
      extends WidgetController 
        implements HitTestDispatcher, TickerProvider {
      WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
        if (binding is LiveTestWidgetsFlutterBinding)
          binding.deviceEventDispatcher = this;
    }
    

    I don't quite get how the Flutter team made this work, but initializing a WidgetTester in the same way they did inside the testWidgets function isn't working for me:

    final TestWidgetsFlutterBinding binding 
      = TestWidgetsFlutterBinding.ensureInitialized() 
        as TestWidgetsFlutterBinding;
    final WidgetTester tester = WidgetTester._(binding);
    

    2. An Example

    A simple example would be to try to break down the tests of the Flutter demo that is created with each new Flutter project from flutter create. In it, we could try to separate the initial setup test of the app from the tapping action test:

    testWidgets('Initial setup', (WidgetTester tester) async {
      await tester.pumpWidget(MyApp());
    
      expect(find.text('0'), findsOneWidget);
      expect(find.text('1'), findsNothing);
    });
    
    testWidgets('Increment the counter on tap', (WidgetTester tester) async {
      await tester.pumpWidget(MyApp());
    
      await tester.tap(find.byIcon(Icons.add));
      await tester.pump();
    
      expect(find.text('0'), findsNothing);
      expect(find.text('1'), findsOneWidget);
    });
    

    The idea would be to try to move the await tester.pumpWidget(MyApp()); into a setUp function.