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