How to test navigation via Navigator in Flutter

后端 未结 3 1382
走了就别回头了
走了就别回头了 2020-12-16 13:00

Let\'s say, I have a test for a screen in Flutter using WidgetTester. There is a button, which executes a navigation via Navigator. I would like to

相关标签:
3条回答
  • 2020-12-16 13:49

    Following solution is, let's say, a general approach and it's not specific to Flutter.

    Navigation could be abstracted away from a screen or a widget. Test can mock and inject this abstraction. This approach should be sufficient for testing such behavior.

    There are several ways how to achieve that. I will show one of those, for purpose of this response. Perhaps it's possible to simplify it a bit or to make it more "Darty".

    Abstraction for navigation

    class AppNavigatorFactory {
      AppNavigator get(BuildContext context) =>
          AppNavigator._forNavigator(Navigator.of(context));
    }
    
    class TestAppNavigatorFactory extends AppNavigatorFactory {
      final AppNavigator mockAppNavigator;
    
      TestAppNavigatorFactory(this.mockAppNavigator);
    
      @override
      AppNavigator get(BuildContext context) => mockAppNavigator;
    }
    
    class AppNavigator {
      NavigatorState _flutterNavigator;
      AppNavigator._forNavigator(this._flutterNavigator);
    
      void showNextscreen() {
        _flutterNavigator.pushNamed('/nextscreen');
      }
    }
    

    Injection into a widget

    class MyScreen extends StatefulWidget {
      final _appNavigatorFactory;
      MyScreen(this._appNavigatorFactory, {Key key}) : super(key: key);
    
      @override
      _MyScreenState createState() => _MyScreenState(_appNavigatorFactory);
    }
    
    class _MyScreenState extends State<MyScreen> {
      final _appNavigatorFactory;
    
      _MyScreenState(this._appNavigatorFactory);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            body: Center(
                child: RaisedButton(
                    onPressed: () {
                        _appNavigatorFactory.get(context).showNextscreen();
                    },
                    child: Text(Strings.traktTvUrl)
                )
            )
        );
      }
    
    }
    

    Example of a test (Uses Mockito for Dart)

    class MockAppNavigator extends Mock implements AppNavigator {}
    
    void main() {
      final appNavigator = MockAppNavigator();
    
      setUp(() {
        reset(appNavigator);
      });
    
    
      testWidgets('Button is present and triggers navigation after tapped',
          (WidgetTester tester) async {
    
        await tester.pumpWidget(MaterialApp(home: MyScreen(TestAppNavigatorFactory())));
    
        expect(find.byType(RaisedButton), findsOneWidget);
        await tester.tap(find.byType(RaisedButton));
    
        verify(appNavigator.showNextscreen());
      });
    }
    
    0 讨论(0)
  • 2020-12-16 13:57

    In the navigator tests in the flutter repo they use the NavigatorObserver class to observe navigations:

    class TestObserver extends NavigatorObserver {
      OnObservation onPushed;
      OnObservation onPopped;
      OnObservation onRemoved;
      OnObservation onReplaced;
    
      @override
      void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
        if (onPushed != null) {
          onPushed(route, previousRoute);
        }
      }
    
      @override
      void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
        if (onPopped != null) {
          onPopped(route, previousRoute);
        }
      }
    
      @override
      void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
        if (onRemoved != null)
          onRemoved(route, previousRoute);
      }
    
      @override
      void didReplace({ Route<dynamic> oldRoute, Route<dynamic> newRoute }) {
        if (onReplaced != null)
          onReplaced(newRoute, oldRoute);
      }
    }
    

    This looks like it should do what you want, however it may only work form the top level (MaterialApp), I'm not sure if you can provide it to just a widget.

    0 讨论(0)
  • 2020-12-16 14:04

    While what Danny said is correct and works, you can also create a mocked NavigatorObserver to avoid any extra boilerplate:

    class MockNavigatorObserver extends Mock implements NavigatorObserver {}
    

    That would translate to your test case as follows:

    void main() {
      testWidgets('Button is present and triggers navigation after tapped',
          (WidgetTester tester) async {
        final mockObserver = MockNavigatorObserver();
        await tester.pumpWidget(
          MaterialApp(
            home: MyScreen(),
            navigatorObservers: [mockObserver],
          ),
        );
    
        expect(find.byType(RaisedButton), findsOneWidget);
        await tester.tap(find.byType(RaisedButton));
        await tester.pumpAndSettle();
    
        /// Verify that a push event happened
        verify(mockObserver.didPush(any, any));
    
        /// You'd also want to be sure that your page is now
        /// present in the screen.
        expect(find.byType(DetailsPage), findsOneWidget);
      });
    }
    

    I wrote an in-depth article about this on my blog, which you can find here.

    0 讨论(0)
提交回复
热议问题