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

Posted

技术标签:

【中文标题】如何在颤振小部件测试中捕获来自未来的错误?【英文标题】:How to catch an error coming from a Future in flutter widget test? 【发布时间】:2019-10-29 05:11:27 【问题描述】:

我在对在 Future 期间引发异常的小部件进行小部件测试时遇到问题。

重现问题的代码

这是一个简单的测试小部件,它以非常简单的方式重现问题(感谢 Remi Rousselet 对问题的简化)。

testWidgets('This test should pass but fails', (tester) async 
  final future = Future<void>.error(42);

  await tester.pumpWidget(FutureBuilder(
    future: future,
    builder: (_, snapshot) 
      return Container();
    ,
  ));
);

预期结果

我希望测试能够顺利完成。相反,它失败并出现以下错误:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The number 42 was thrown running a test.

When the exception was thrown, this was the stack:
#2      main.<anonymous closure> (file:///C:/Projects/projet_65/mobile_app/test/ui/exception_test.dart:79:18)
#5      TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:0:0)
#8      TestWidgetsFlutterBinding._runTest (package:flutter_test/src/binding.dart:577:14)
#9      AutomatedTestWidgetsFlutterBinding.runTest.<anonymous closure> (package:flutter_test/src/binding.dart:993:24)
#15     AutomatedTestWidgetsFlutterBinding.runTest (package:flutter_test/src/binding.dart:990:15)
#16     testWidgets.<anonymous closure> (package:flutter_test/src/widget_tester.dart:106:22)
#17     Declarer.test.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/declarer.dart:168:27)
#20     Declarer.test.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/declarer.dart:0:0)
#21     Invoker.waitForOutstandingCallbacks.<anonymous closure> (package:test_api/src/backend/invoker.dart:250:15)
#27     Invoker._onRun.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/invoker.dart:399:21)
(elided 17 frames from class _FakeAsync, package dart:async, and package dart:async-patch)

The test description was:
  This test should pass but fails
════════════════════════════════════════════════════════════════════════════════════════════════════

我的尝试

我已经尝试expect 错误,如果它不在Future 中,我会这样做:

expect(tester.takeException(), equals(42));

但该语句失败并出现以下错误

The following TestFailure object was thrown running a test (but after the test had completed):
  Expected: 42
  Actual: null

编辑:

@shb 答案对于暴露的情况是正确的。这是一个轻微的修改,可以打破它并返回初始错误。这种情况在实际应用中更容易发生(我就是这种情况)

  testWidgets('This test should pass but fails', (tester) async 
      Future future;

      await tester.pumpWidget(
        MaterialApp(
          home: Row(
            children: <Widget>[
              FlatButton(
                child: const Text('GO'),
                onPressed: ()  future = Future.error(42);,
              ),
              FutureBuilder(
                future: future,
                builder: (_, snapshot) 
                  return Container();
                ,
              ),
            ],
          ),
        ));

      await tester.tap(find.text('GO'));
  );

注意:我自愿省略了@shb 提出的tester.runAsync 以匹配最初的问题,因为它在特定情况下不起作用

【问题讨论】:

(没关系,忽略我之前的cmets。) @Muldec 如果将tester.tap 包裹在runAsync 中会发生什么? @RémiRousselet 相同的错误加上一个新的The following assertion was thrown running a test (but after the test had completed): 'package:flutter_test/src/binding.dart': Failed assertion: line 1020 pos 12: '_currentFakeAsync !=null': is not true.(意味着测试引发了两个错误) 【参考方案1】:

await tester.runAsync(() async .. 包装你的代码

来自official documentationrunAsync&lt;T&gt;

运行一个执行真正异步工作的回调。

这适用于需要调用异步方法的调用者 其中方法产生隔离或操作系统线程,因此不能 通过调用pump同步执行。

见下文

testWidgets('This test should pass but fails', (tester) async 

    await tester.runAsync(() async 

      final future = Future<void>.error(42);

      await tester.pumpWidget(FutureBuilder(
        future: future,
        builder: (_, snapshot) 
          return Container();
        ,
      ));

    );

  );

编辑:

(OP 提出的第二个问题)

在这种情况下使用

Future.delayed(Duration.zero, () 
   tester.tap(find.text('GO'));
);

下面是完整的sn-p

testWidgets('2nd try This test should pass but fails', (tester) async 

Future future;
await tester.runAsync(() async 
  await tester.pumpWidget(
    MaterialApp(
      home: Row(
        children: <Widget>[
          FlatButton(
            child: const Text('GO'),
            onPressed: () 
              future = Future.error(42);
            ,
          ),
          FutureBuilder(
            future: future,
            builder: (_, snapshot) 
              return Container();
            ,
          ),
        ],
      ),
    ),
  );

  Future.delayed(Duration.zero, () tester.tap(find.text('GO')););
);
);

编辑 2:

后来发现

Future.delayed(Duration.zero, () tester.tap(find.text('GO')); );

没有被调用。

【讨论】:

感谢您的回答。这解决了最初提出的问题。我编辑了帖子以记入您的答案并添加了此解决方案不起作用的代码(问题最初已被过度简化)。你怎么看? 测试没有失败,但Future.delayed 中的Future 也没有被调用。我添加了print 语句来检查代码是否运行,但控制台上没有显示任何内容。 @Muldec 是的,看起来如此。我已经相应地编辑了答案 你试过等待未来吗?也可以考虑Future.microtask,而不是Future.delayed @RémiRousselet Future.microtask 确实执行但它也不起作用【参考方案2】:

期货向其听众报告错误。如果Future 没有侦听器,它会通知Zone 未捕获的错误(src)。这是测试框架从中获取错误的地方。

克服此错误的一种方法是在错误 Future 之前等待侦听器。

  testWidgets('This passes', (tester) async 
    final Completer completer = Completer();
    await tester.pumpWidget(FutureBuilder(
      future: completer.future,
      builder: (_, snapshot) 
        return Container();
      ,
    ));

    // has subscribers, doesn't inform Zone about uncought error
    completer.completeError(42);
    tester.pumpAndSettle();
  );

【讨论】:

如果您可以控制错误,这是正确的。根据特定情况抛出错误的代码怎么样?你会怎么处理?我用稍微复杂的代码更新了我的问题。【参考方案3】:

我认为问题在于您没有发现错误并导致应用程序崩溃。

我已经尝试捕捉错误并且测试通过了:

代码如下:


testWidgets('This test should pass but fails', (tester) async 
    Future future;

    await tester.pumpWidget(MaterialApp(
      home: Row(
        children: <Widget>[
          FlatButton(
            child: const Text('GO'),
            onPressed: () 
              future = Future.error(42).catchError((error) );
            ,
          ),
          FutureBuilder(
            future: future,
            builder: (_, snapshot) 
              return Container();
            ,
          ),
        ],
      ),
    ));

    await tester.tap(find.text('GO'));
  );

【讨论】:

是和不是。当future builder中出现错误时,它将数据作为null。但是对于这种情况,Future 本身应该使用 catchError。 @SergioBernal 的观点似乎有效。 Future.value(42) 的测试通过; 问题是:Future 上的catchError 处理错误并破坏了FutureBuilder.buildersnapshot.hasError 的点。如果错误已被处理,这将返回 false。因此,测试(和使用)FutureBuilder 的行为变得不清楚为什么向用户显示的错误消息是在 !snapshot.hasData 而不是在 snapshot.hasError 上完成的。 @SergioBernal FutureBuilder 已经在Future 上调用了catchError。问题不在于它没有被捕获,而是被异步捕获。

以上是关于如何在颤振小部件测试中捕获来自未来的错误?的主要内容,如果未能解决你的问题,请参考以下文章

如何在颤振小部件测试中发出 http 请求?

颤振小部件测试 - 如何执行“返回上一个屏幕”的测试

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

如何修复“必须向文本小部件提供非空字符串”。颤振错误?

颤振错误:“小部件无法构建,因为已经在构建过程中”

如何在颤振小部件中使用条件语句/三元