How to test navigation via Navigator in Flutter How to test navigation via Navigator in Flutter flutter flutter

How to test navigation via Navigator in Flutter


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

import 'package:mockito/mockito.dart';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.


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.


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());  });}