Disons que j'ai un test pour un écran dans Flutter en utilisant WidgetTester
. Il y a un bouton, qui exécute une navigation via Navigator
. Je voudrais tester le comportement de ce bouton.
Widget/écran
class MyScreen extends StatefulWidget {
MyScreen({Key key}) : super(key: key);
@override
_MyScreenState createState() => _MyScreenScreenState();
}
class _MyScreenState extends State<MyScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.of(context).pushNamed("/nextscreen");
},
child: Text(Strings.traktTvUrl)
)
)
);
}
}
test
void main() {
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: MyScreen()));
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
//how to test navigator?
});
}
Y a-t-il un moyen approprié de vérifier que ce navigateur a été appelé? Ou existe-t-il un moyen de se moquer et de remplacer le navigateur?
Veuillez noter que le code ci-dessus échouera réellement avec une exception, car il n'y a pas de route nommée '/nextscreen'
déclaré en application. C'est simple à résoudre et vous n'avez pas besoin de le signaler.
Ma principale préoccupation est de savoir comment aborder correctement ce scénario de test dans Flutter.
Dans la classe navigator tests in the flutter repo ils utilisent la classe NavigatorObserver pour observer les 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);
}
}
Il semble que cela devrait faire ce que vous voulez, mais cela ne peut fonctionner que du niveau supérieur (MaterialApp), je ne sais pas si vous pouvez le fournir à un widget.
Bien que ce que Danny a dit est correct et fonctionne, vous pouvez également créer un NavigatorObserver simulé pour éviter tout passe-partout supplémentaire:
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
Cela se traduirait dans votre cas de test comme suit:
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(typed(any), typed(any)));
/// You'd also want to be sure that your page is now
/// present in the screen.
expect(find.byType(DetailsPage), findsOneWidget);
});
}
J'ai écrit un article détaillé à ce sujet sur mon blog, que vous pouvez trouver ici .
La solution suivante est, disons, une approche générale et elle n'est pas spécifique à Flutter.
La navigation peut être abstraite loin d'un écran ou d'un widget. Le test peut se moquer et injecter cette abstraction. Cette approche devrait être suffisante pour tester un tel comportement.
Il existe plusieurs façons d'y parvenir. Je vais en montrer un, aux fins de cette réponse. Il est peut-être possible de le simplifier un peu ou de le rendre plus "Darty".
Abstraction pour la 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 dans un 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)
)
)
);
}
}
Exemple de test (Utilise 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());
});
}