如何在 Flutter 小部件测试中等待未来完成?

Posted

技术标签:

【中文标题】如何在 Flutter 小部件测试中等待未来完成?【英文标题】:How to wait for a future to complete in Flutter widget test? 【发布时间】:2019-02-10 02:32:51 【问题描述】:

我正在尝试执行小部件测试,特别是导航测试。我正在使用 bloc 架构,在 bloc 上设置流会触发 bloc 内的一系列事件,从服务器调用获取会话信息(返回会话信息对象的未来),在成功的服务器调用上设置登录流并且小部件已订阅此流并导航到下一个屏幕。

我正在使用 mockito 来模拟服务器调用并存根服务器调用以返回成功响应的未来。问题是当我调用 pumpAndSettle() 时它会超时,因为它没有等待未来完成并返回成功响应。

如果我没有说得很清楚,我很抱歉,但这里是示例代码:

login_bloc.dart

class LoginBloc 
  LoginRepository _loginRepository;
  final String searchKeyword = "special-keyword";

  final _urlString = PublishSubject<String>();
  final _isLoggedIn = BehaviorSubject<bool>(seedValue: false);
  final _errorMessage = PublishSubject<String>();

  Observable<bool> get isLoggedIn => _isLoggedIn.stream;
  Observable<String> get isErrorState => _errorMessage.stream;

  LoginBloc(LoginRepository loginRepository)
      : _loginRepository = loginRepository ?? LoginRepository() 
          // Listen on the _urlString stream to call the function which checks for the special keyword and if a match is found make a server call
    _urlString.stream.listen((String url) 
      _authorizationFullService(url);
    );
  

    // Search for special keyword and if a match is found call the server call function
  void _authorizationFullService(String url) 
    if (url.contains(searchKeyword)) 
      int index = url.indexOf(searchKeyword);
      String result = url.substring(index + searchKeyword.length);
      result = result.trim();
      String decodedUrl = Uri.decodeFull(result);
      if (decodedUrl != null && decodedUrl.length > 0) 
        _fullServiceServerCall(decodedUrl);
       else 
        _isLoggedIn.sink.add(false);
      
    
  

    // Call server call function from repository which returns a future of the Authorization object
  void _fullServiceServerCall(String decodedUrl) 
    _loginRepository
        .getSession(decodedUrl)
        .then(_handleSuccessAuthorization)
        .catchError(_handleErrorState);
  

    // Handle success response and set the login stream
  void _handleSuccessAuthorization(Authorization authorization) 
    if (authorization != null && authorization.idnumber != 0) 
      _isLoggedIn.sink.add(true);
     else 
      _isLoggedIn.sink.add(false);
    
  

    // Handle error response and set the error stream
  void _handleErrorState(dynamic error) 
    _isLoggedIn.sink.add(false);
    _errorMessage.sink.add(error.toString());
  

  void dispose() 
    _urlString.close();
    _isLoggedIn.close();
    _errorMessage.close();
  

widget_test.dart

group('Full Login Navigation test', () 
    LoginRepository mockLoginRepository;
    LoginBloc loginBloc;
    NotificationBloc notificationBloc;
    NavigatorObserver mockNavigatorObserver;
    Authorization _auth;
    String testUrl;

    setUp(() 
      mockLoginRepository = MockLoginRepository();
      _auth = Authorization((auth) => auth
        ..param1 = "foo"
        ..param2 = "bar"
        ..param3 = "foobar"
        ..param4 = "barfoo");
      loginBloc = LoginBloc(loginRepository: mockLoginRepository);
      mockNavigatorObserver = MockNavigatorObserver();
      testUrl = "http://test.test.com";
    );

    Future<Null> _buildFullLoginPage(LoginBloc loginBloc,
        NotificationBloc notificationBloc, WidgetTester tester) async 
      when(mockLoginRepository.getSession(testUrl))
          .thenAnswer((_) => Future.value(_auth));
      await tester.pumpWidget(LoginBlocProvider(
        child: NotificationBlocProvider(
          child: MaterialApp(
            home: LoginFullService(),
            onGenerateRoute: NavigationRoutes.routes,
            navigatorObservers: [mockNavigatorObserver],
          ),
          notificationBloc: notificationBloc,
        ),
        loginBloc: loginBloc,
      ));
      //TODO: Remove casting to dynamic after dart sdk bug fix: https://github.com/dart-lang/mockito/issues/163
      verify(mockNavigatorObserver.didPush(any, any) as dynamic);
      loginBloc.getAuthorization(
          "http://testing.testing.com?search-keyword=http%3A%2F%2Ftest.test.com");
    

    testWidgets('Navigate to landing page on correct login url',
        (WidgetTester tester) async 
      await _buildFullLoginPage(loginBloc, notificationBloc, tester);
      await tester.pumpAndSettle();
      expect(find.byKey(Key('webview_scaffold')), findsNothing);
      //TODO: Remove casting to dynamic after dart sdk bug fix: https://github.com/dart-lang/mockito/issues/163
      verify(mockNavigatorObserver.didPush(any, any) as dynamic);
    );
);

在运行小部件测试时,testWidgets 内部的tester.pumpAndSettle() 在未来完成之前超时。这是错误日志:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
pumpAndSettle timed out

When the exception was thrown, this was the stack:
#0      WidgetTester.pumpAndSettle.<anonymous closure> (package:flutter_test/src/widget_tester.dart:299:11)
<asynchronous suspension>
#3      TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:69:41)
#4      WidgetTester.pumpAndSettle (package:flutter_test/src/widget_tester.dart:295:27)
#5      main.<anonymous closure>.<anonymous closure> (file:///Users/ssiddh/Documents/projects/mobile-flutter/test/ui/pages/login/login_full_test.dart:114:20)
<asynchronous suspension>
#6      testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:72:23)
#7      TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:555:19)
<asynchronous suspension>
#10     TestWidgetsFlutterBinding._runTest (package:flutter_test/src/binding.dart:539:14)
#11     AutomatedTestWidgetsFlutterBinding.runTest.<anonymous closure> (package:flutter_test/src/binding.dart:883:24)
#17     AutomatedTestWidgetsFlutterBinding.runTest (package:flutter_test/src/binding.dart:880:15)
#18     testWidgets.<anonymous closure> (package:flutter_test/src/widget_tester.dart:71:22)
#19     Declarer.test.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test/src/backend/declarer.dart:168:27)
<asynchronous suspension>
#20     Invoker.waitForOutstandingCallbacks.<anonymous closure> (package:test/src/backend/invoker.dart:249:15)
<asynchronous suspension>
#25     Invoker.waitForOutstandingCallbacks (package:test/src/backend/invoker.dart:246:5)
#26     Declarer.test.<anonymous closure>.<anonymous closure> (package:test/src/backend/declarer.dart:166:33)
#31     Declarer.test.<anonymous closure> (package:test/src/backend/declarer.dart:165:13)
<asynchronous suspension>
#32     Invoker._onRun.<anonymous closure>.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test/src/backend/invoker.dart:403:25)
<asynchronous suspension>
#46     _Timer._runTimers (dart:isolate/runtime/libtimer_impl.dart:382:19)
#47     _Timer._handleMessage (dart:isolate/runtime/libtimer_impl.dart:416:5)
#48     _RawReceivePortImpl._handleMessage (dart:isolate/runtime/libisolate_patch.dart:169:12)
(elided 30 frames from class _FakeAsync, package dart:async, and package stack_trace)

非常感谢任何形式的帮助或反馈。

【问题讨论】:

【参考方案1】:

尝试用

包装你的测试
testWidgets('Navigate to landing page on correct login url',
    (WidgetTester tester) async 
    await tester.runAsync(() async 

      // test code here

    );
 );

【讨论】:

你刚刚拯救了我的一天!干杯! 您能解释一下您的解决方案吗? Flutter 测试使用 fakeAsync,这意味着 Futures/Streams 不会在没有额外推送的情况下执行。这允许它用于例如等待一段时间(延迟)以假装时间已经过去的测试。这允许单元测试运行得更快。但没有这个,他们将永远等待。 runAsync 恢复“正常”行为。我希望我没看错,我有一段时间仔细观察了。 嘿,你能告诉我一种方法,我可以等待我的 Api 响应并且 bool 标志值是真的然后做某事,我怎么能在 widgettest 中做到这一点 @Bunny1367 请创建一个包含更多信息的新问题

以上是关于如何在 Flutter 小部件测试中等待未来完成?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Flutter 小部件测试中执行缩放(捏缩放)多指手势?

如何在 Flutter 小部件中测试回调函数

Flutter - 如何测试小部件中的抽屉测试?

Flutter - 如何在小部件测试中选择 DropdownButton 项

如何在颤振小部件测试中捕获来自未来的错误?

在 Flutter 小部件测试中,如何验证字段验证错误消息?