web-dev-qa-db-ja.com

Flutterのナビゲーターでナビゲーションをテストする方法

たとえば、WidgetTesterを使用してFlutterの画面のテストを行ったとします。 Navigatorを介してナビゲーションを実行するボタンがあります。そのボタンの動作をテストしたいと思います。

ウィジェット/画面

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

}

テスト

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

ナビゲーターが呼び出されたことを確認する適切な方法がありますか?または、ナビゲーターをモックして置き換える方法はありますか?

アプリケーションで宣言された名前付きルート'/nextscreen'がないため、上記のコードは実際には例外で失敗します。これは簡単に解決でき、指摘する必要もありません。

私の主な関心事は、Flutterでこのテストシナリオに正しくアプローチする方法です。

10
Josef Adamcik

フラッターリポジトリのナビゲーターテスト では、ナビゲーションを監視するために NavigatorObserver クラスを使用します。

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

これはあなたが望むことをするように見えますが、それはトップレベル(MaterialApp)からしか機能しないかもしれません。ウィジェットだけに提供できるかどうかはわかりません。

6
Danny Tuppeny

ダニーが言ったことは正しく機能していますが、偽のNavigatorObserverを作成して余分なボイラープレートを回避することもできます。

class MockNavigatorObserver extends Mock implements NavigatorObserver {}

これは、次のようにテストケースに変換されます。

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

私はこれについてブログで詳細な記事を書きました ここで見つけることができます

30
Iiro Krankka

次の解決策は、一般的なアプローチであり、Flutterに固有のものではありません。

画面またはウィジェットからナビゲーションを抽象化することができます。テストはこの抽象化をモックして注入することができます。このアプローチは、そのような動作をテストするのに十分なはずです。

それを達成する方法はいくつかあります。この応答のために、そのうちの1つを示します。おそらくそれを少し単純化するか、より「ダーティ」にすることが可能です。

ナビゲーションの抽象化

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

ウィジェットへの挿入

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

}

テストの例(使用 DartのMockito

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