web-dev-qa-db-fra.com

Comment tester la navigation via Navigator dans Flutter

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.

10
Josef Adamcik

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.

6
Danny Tuppeny

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 .

30
Iiro Krankka

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