【问题标题】:How to correctly mock NavigatorObserver in Flutter with Mockito?如何使用 Mockito 在 Flutter 中正确模拟 NavigatorObserver?
【发布时间】:2020-04-25 02:51:09
【问题描述】:

我有一个测试,当应用程序从contacts_footer.dart 导航到create_and_edit_contact.dart(推送)和返回(弹出)时,我试图观察[Navigator] 的行为。使用 Mockito 包中的verify,我可以成功验证推送行为是否有效,但是验证弹出行为失败。 _navigateToBack 函数按预期工作,对仅出现在 contacts_footer.dart 中的小部件的测试成功,但观察弹出行为失败。

contacts_footer.dart

 class ContactsFooter extends StatelessWidget {
  static const navigateToEditPage = Key('navigateEdit');
  const ContactsFooter({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BottomAppBar(
      color: Color.fromRGBO(244, 244, 244, 1),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          IconButton(
            icon: Icon(Icons.edit),
            onPressed: () {
              Provider.of<Contacts>(context).setEditMode(true);
              Navigator.of(context).push(MaterialPageRoute(
                  builder: (context) => CreateAndEditContact()));
            },
            key: ContactsFooter.navigateToEditPage,
          )
        ],
      ),
    );
  }
}

create_and_edit_contact.dart


class CreateAndEditContact extends StatefulWidget {
  static const routeName = 'edit-contact';
  static var editOrCreateDetails = Key('editOrCreate');

  @override
  _CreateAndEditContactState createState() => _CreateAndEditContactState();
}

class _CreateAndEditContactState extends State<CreateAndEditContact> {
...
Widget build(BuildContext context) {
    _isEditMode = Provider.of<Contacts>(context).isEditMode;
...
return Scaffold (
..
RaisedButton(key: CreateAndEditContact.editOrCreateDetails,
             onPressed: () {
                   ....      
                            if (_isEditMode) {
                              print(true);
                              Provider.of<Contacts>(context)
                                  .updateContact(context,formData, _selectedContact.vUuid)
                                  .then((data) {
                                Navigator.of(context).pop();
                              });
                            } else {
                              print(false);
                              Provider.of<Contacts>(context)
                                  .createContact(context,formData)
                                  .then((data) {
                                Navigator.of(context).pop();
                              }).catchError(
                                (error) => showDialog(
                                  context: context,
                                  builder: (context) => ErrorDialog(
                                    error.toString(),
                                  ),
                                ),
                              );
                            }
                          },
                          child: Text(
                            'Sumbit',
                            style: TextStyle(color: Colors.white),
                          ),
                          color: Theme.of(context).accentColor,

                        ),
)

}

}

test file

group('EditPage navigation tests', () {
    NavigatorObserver mockObserver;


    setUp(() {
      mockObserver = MockNavigatorObserver();

    });

    Future<Null> _buildMainPage(WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        home: Scaffold(
          body: Builder(
            builder: (context) => Center(
              child: MultiProvider(
      providers: [
        ChangeNotifierProvider<Contacts>(create: (_) => Contacts()),
      ],
      child: Builder(
        builder: (_) => MaterialApp(home: ContactsFooter(),
      ),
    );,
            ),
          ),
        ),

        /// This mocked observer will now receive all navigation events
        /// that happen in our app.
        navigatorObservers: <NavigatorObserver>[mockObserver],
      ));

      /// The tester.pumpWidget() call above just built our app widget
      /// and triggered the pushObserver method on the mockObserver once.


    }

    Future<Null> _navigateToDetailsPage(WidgetTester tester) async {
      /// Tap the button which should navigate to the edit details page.
      /// By calling tester.pumpAndSettle(), we ensure that all animations
      /// have completed before we continue further.

      await tester.tap(find.byKey(ContactsFooter.navigateToEditPage));
      await tester.pumpAndSettle();

    }

    Future<Null> _navigateToBack(WidgetTester tester) async {
      await tester.tap(find.byKey(CreateAndEditContact.editOrCreateDetails));
      int num = await tester.pumpAndSettle();
      print(num);
    }

    testWidgets(
        'when tapping "navigate to edit details" button, should navigate to details page',
        (WidgetTester tester) async {
      await _buildMainPage(tester);

      //CreateAndEditContact widget not present on screen as push event is not triggered yet
      expect(find.byType(CreateAndEditContact), findsNothing);
      //Trigger push event
      await _navigateToDetailsPage(tester);

      // By tapping the button, we should've now navigated to the edit details
      // page. The didPush() method should've been called...
      final Route pushedRoute =
          verify(mockObserver.didPush(captureAny, any)).captured.single;
      print(pushedRoute);


      // there should be a CreateAndEditContact page present in the widget tree...
      var createAndEdit = find.byType(CreateAndEditContact);
      expect(createAndEdit, findsOneWidget);



      await _navigateToBack(tester);

      verify(mockObserver.didPop(any, any));
      expect(find.byType(CreateAndEditContact), findsNothing);

      expect(find.byKey(ContactsFooter.navigateToEditPage), findsWidgets);


    });
}

所有期望语句都正确执行。但是verify(mockObserver.didPop(any, any)); 会导致异常,好像[NavigatorObserver] 无法识别弹出行为。

>(RouteSettings("/", null), animation: AnimationController#1a3c6(⏭ 1.000; paused; for MaterialPageRoute<dynamic>(/)))
5
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure object was thrown running a test:
  No matching calls. All calls: MockNavigatorObserver.navigator,
MockNavigatorObserver._navigator==NavigatorState#36772(tickers: tracking 1 ticker), [VERIFIED]
MockNavigatorObserver.didPush(MaterialPageRoute<dynamic>(RouteSettings("/", null), animation:
AnimationController#1a3c6(⏭ 1.000; paused; for MaterialPageRoute<dynamic>(/))), null)
(If you called `verify(...).called(0);`, please instead use `verifyNever(...);`.)

When the exception was thrown, this was the stack:
#0      fail (package:test_api/src/frontend/expect.dart:153:30)
#1      _VerifyCall._checkWith (package:mockito/src/mock.dart:648:7)
#2      _makeVerify.<anonymous closure> (package:mockito/src/mock.dart:935:18)
#3      main.<anonymous closure>.<anonymous closure> (file:///Users/calvin.gonsalves/Projects/Flutter/Dec23-2019/cmic_mobile_field/test/main_widget_test.dart:316:13)
<asynchronous suspension>
#4      testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:124:25)
#5      TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:696:19)
<asynchronous suspension>
#8      TestWidgetsFlutterBinding._runTest (package:flutter_test/src/binding.dart:679:14)
#9      AutomatedTestWidgetsFlutterBinding.runTest.<anonymous closure> (package:flutter_test/src/binding.dart:1050:24)
#15     AutomatedTestWidgetsFlutterBinding.runTest (package:flutter_test/src/binding.dart:1047:15)
#16     testWidgets.<anonymous closure> (package:flutter_test/src/widget_tester.dart:121:22)
#17     Declarer.test.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/declarer.dart:171:27)
<asynchronous suspension>
#18     Invoker.waitForOutstandingCallbacks.<anonymous closure> (package:test_api/src/backend/invoker.dart:242:15)
#23     Invoker.waitForOutstandingCallbacks (package:test_api/src/backend/invoker.dart:239:5)
#24     Declarer.test.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/declarer.dart:169:33)
#29     Declarer.test.<anonymous closure> (package:test_api/src/backend/declarer.dart:168:13)
#30     Invoker._onRun.<anonymous closure>.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/invoker.dart:392:25)
#44     _Timer._runTimers (dart:isolate-patch/timer_impl.dart:384:19)
#45     _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:418:5)
#46     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:174:12)
(elided 28 frames from class _FakeAsync, package dart:async, package dart:async-patch, and package stack_trace)

The test description was:
  when tapping "navigate to edit details" button, should navigate to details page
════════════════════════════════════════════════════════════════════════════════════════════════════
Test failed. See exception logs above.
The test description was: when tapping "navigate to edit details" button, should navigate to details page

✖ EditPage navigation tests when tapping "navigate to edit details" button, should navigate to details page

我推荐了https://iirokrankka.com/2018/08/22/writing-widget-tests-for-navigation-events/

【问题讨论】:

  • 在您的_buildMainPage 方法中,您围绕ContactsFooter 小部件创建一个嵌套的MaterialApp 小部件。你试过删除它吗?
  • @JordanDavies 我需要嵌套,因为Scaffold 需要外部MaterialApp,它提供了正确的上下文,删除内部MaterialApp 会导致Provider&lt;Contacts&gt; not found error。但是,我想我通过将navigatorObservers 移动到内部MaterialApp 找到了解决方案。此外,我需要验证在_buildMainPage 之后但在_navigateToDetailsPage 之前发生的推送事件。现在可以正确观察到弹出行为。
  • 您能否将问题标记为已回答?谢谢

标签: flutter mockito flutter-test


【解决方案1】:

这个问题似乎是由Navigator 缺少上下文引起的——在这种情况下通常由MaterialApp 提供。正如您在 cmets 中提到的,将 navigatorObservers 移动到 MaterialApp 中可以解决问题。另一种解决方法是使用navigatorKey 直接管理Navigator,而无需先从BuildContext 获取它。请参阅此similar Stack Overflow post,了解如何使用navigatorKey

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-02-02
    • 2021-08-07
    • 1970-01-01
    • 1970-01-01
    • 2018-11-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多