【问题标题】:How to test navigation via Navigator in Flutter如何在 Flutter 中通过 Navigator 测试导航
【发布时间】:2018-11-15 05:01:48
【问题描述】:

假设我在 Flutter 中使用 WidgetTester 测试了一个屏幕。有一个按钮,通过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 中正确处理这个测试场景。

【问题讨论】:

    标签: dart flutter flutter-test


    【解决方案1】:

    虽然 Danny 所说的正确且有效,但您也可以创建一个模拟的 NavigatorObserver 以避免任何额外的样板:

    import 'package:mockito/mockito.dart';
    
    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(any, any));
    
        /// You'd also want to be sure that your page is now
        /// present in the screen.
        expect(find.byType(DetailsPage), findsOneWidget);
      });
    }
    

    我在我的博客which you can find here 上写了一篇关于此的深入文章。

    【讨论】:

    • 那么在这种情况下拥有 NavigatorObserver 有什么意义呢?如果您想查看您是否登陆了正确的页面,您只需检查 finder 是否找到了一个“DetailsPage”类型的小部件。
    • 除了推送事件之外,DetailsPage 还可以通过其他方式变得可见。 MyScreen 可能会意外地有一个 .pushReplacement() 来电。这将用DetailsPage 替换MyScreen,并且没有办法再回到MyScreen。这可能不是预期的行为。
    • 要使用此建议,请在您的 pubspec.yaml 的 dev_dependencies 部分中导入 mockito
    • The argument type 'Null' can't be assigned to the parameter type 'Route&lt;dynamic&gt;'
    • 如果可以的话,使用 Mockito 的 GenerateMocks 和 build_runner 来生成一个 mock NavigatorObserver。例如。 @GenerateMocks([], customMocks: [ MockSpec&lt;NavigatorObserver&gt;(returnNullOnMissingStub: true), ], )。这个 MockNavigatorObserver 允许您传递返回 null 的参数匹配器。
    【解决方案2】:

    navigator tests in the flutter repo 中,他们使用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),我不确定你是否可以将它提供给一个小部件。

    【讨论】:

    • 这绝对是一种方法,谢谢指出。但是,我觉得经常使用时使用起来会有点乏味并且难以在测试中阅读。也许我可以尝试找到一些巧妙的方法将其隐藏在一些帮助程序或语法糖后面。或者我可能只是将 Navigator 封装在自定义抽象中并模拟它。
    • @JosefAdamcik 一种“不那么乏味的方式”将用 mockito 模拟 NavigatorObserver,如以下答案所示:stackoverflow.com/a/51983194/940036
    【解决方案3】:

    比方说,以下解决方案是一种通用方法,并不特定于 Flutter。

    导航可以从屏幕或小部件中抽象出来。测试可以模拟和注入这个抽象。这种方法应该足以测试此类行为。

    有几种方法可以实现这一目标。我将展示其中的一个,以便做出回应。也许可以稍微简化一下,或者让它更“Darty”。

    导航抽象

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

    测试示例(使用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());
      });
    }
    

    【讨论】:

      猜你喜欢
      • 2021-03-16
      • 1970-01-01
      • 1970-01-01
      • 2020-07-21
      • 2022-07-10
      • 2021-01-21
      • 1970-01-01
      • 2021-08-22
      • 1970-01-01
      相关资源
      最近更新 更多