如何在流监听器中对异步函数调用进行单元测试。

huangapple go评论62阅读模式
英文:

How to unit test async function call inside stream listener

问题

Consider the following code that needs to be unit tested:

void run() {
    _activityRepo.activityUpdateStream.listen((token) async {
      await _userRepo.updateToken(token: token);
    });
}

where _activityRepo.activityUpdateStream is a Stream<String> that emits String events.

The goal here is to test that updateToken function is called every time _activityRepo.activityUpdateStream emits a new event. The unit test is currently like:

test('should update notification token when refreshed', () async {
    const fakePushToken = 'token';
    final fakeStream = StreamController<String>();

    when(() => liveActivityRepo.activityUpdateStream).thenAnswer((_) => fakeStream.stream);

    useCase.run();

    fakeStream.add(fakeUpdate);

    verify(() => userRepo.updateLiveActivityToken(token: fakePushToken)).called(1);
});

This test fails because verify is being called before updateToken function inside the stream listener. How should I rewrite this test so I can make sure the stream listener is fully executed before running the verification step in the test?

英文:

Consider the following code that needs to be unit tested

void run() {
    _activityRepo.activityUpdateStream.listen((token) async {
      await _userRepo.updateToken(token: token);
    });
  }

where _activityRepo.activityUpdateStream is a Stream&lt;String&gt; that emits String events.

The goal here is to test that updateToken function is called every time _activityRepo.activityUpdateStream emits a new event. The unit test is currently like:

test(&#39;should update notification token when refreshed&#39;, () async {
    const fakePushToken = &#39;token&#39;;
    final fakeStream = StreamController&lt;String&gt;();

    when(() =&gt; liveActivityRepo.activityUpdateStream).thenAnswer((_) =&gt; fakeStream.stream);

    useCase.run();

    fakeStream.add(fakeUpdate);

    verify(() =&gt; userRepo.updateLiveActivityToken(token: fakePushToken)).called(1);
  });

This test fails because verify is being called before updateToken function inside the stream listener. How should I rewrite this test so I can make sure the stream listener is fully executed before running the verification step in the test?

答案1

得分: 1

这个单元测试失败,因为正如你解释的那样,在流监听器的异步操作执行之前,verify 被执行。

这些异步操作可能需要一些时间,因此依赖于某种计时器(包括处理超时的计时器)。考虑远程 API 调用。这些操作被放入一个叫做事件队列的东西中,因为它们的完成依赖于一些外部事件的发生。

但它们可能只是一些完成得很快的延迟操作,称为微任务。它们可能令人困惑,因为它们看起来像同步操作,我们可以期望它们同步完成。它们被放入一个叫做微任务队列的东西中。

我不知道你的 updateToken 执行了什么样的异步任务,因此我会给你两种解决方案。

我制作了这个例子:

import 'package:fake_async/fake_async.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';

main() {
  test("Sync with timers", () {
    int n = 0;
    var stream = Stream.fromIterable([1, 2, 3, 4, 5]);
    stream.listen((value) async {
      await Future.delayed(Duration(seconds: value));
      n = value;
      debugPrint(value.toString());
    });
    expect(n, 0);
  });

  test("Sync with microtasks", () {
    int n = 0;
    var stream = Stream.fromIterable([1, 2, 3, 4, 5]);
    stream.listen((value) async {
      n = value;
      debugPrint(value.toString());
    });
    expect(n, 0);
  });

  test("Fake async with timers", () {
    fakeAsync((fa) {
      int n = 0;
      var stream = Stream.fromIterable([1, 2, 3, 4, 5]);
      stream.listen((value) async {
        await Future.delayed(Duration(seconds: value));
        n = value;
        debugPrint(value.toString());
      });
      fa.flushTimers();
      expect(n, 5);
    });
  });

  test("Fake async with microtasks", () {
    fakeAsync((fa) {
      int n = 0;
      var stream = Stream.fromIterable([1, 2, 3, 4, 5]);
      stream.listen((value) async {
        n = value;
        debugPrint(value.toString());
      });
      fa.flushMicrotasks();
      expect(n, 5);
    });
  });
}

前两个测试展示了在同步单元测试中我们期望发生的事情,就像你所做的那样。n 不会在调用 expect 之前增加。第二个测试最令人惊讶,因为即使我们的“异步”任务实际上是同步的(它只是分配一个值并打印它),它也只会在同步语句之后运行,包括 expect

通过使用 fake_async 包,你可以操纵事件队列和微任务队列。你的单元测试嵌套在一个 fakeAsync 调用中,通过调用 flushTimersflushMicrotasks,你可以完成流监听器的所有异步操作。

请注意,如果你交换了对 flushTimersflushMicrotasks 的调用,fakeAsync 测试将会失败,这表明你需要调用它们中的哪一个取决于异步任务的具体性质。

英文:

This unit test fails because, as you explained, verify is executed before the asynchronous operations of the stream listener.

Those asynchronous operations may take some time, and therefore depend on some kind of timers (including those that handle timeouts). Think remote API calls. Those operations are put in something called the Event Queue because their completion depend on some external events happening.

But they may just be some delayed operations that complete quickly, called microtasks. They can be confusing because they look like synchronous operations and we can expect them to complete synchronously. They are put in something called the Microtask Queue.

I don't know what kind of async task your updateToken performs, therefore I'm giving you both solutions.

I made this example:

import &#39;package:fake_async/fake_async.dart&#39;;
import &#39;package:flutter/foundation.dart&#39;;
import &#39;package:flutter_test/flutter_test.dart&#39;;

main() {
  test(&quot;Sync with timers&quot;, () {
    int n = 0;
    var stream = Stream.fromIterable([1, 2, 3, 4, 5]);
    stream.listen((value) async {
      await Future.delayed(Duration(seconds: value));
      n = value;
      debugPrint(value.toString());
    });
    expect(n, 0);
  });

  test(&quot;Sync with microtasks&quot;, () {
    int n = 0;
    var stream = Stream.fromIterable([1, 2, 3, 4, 5]);
    stream.listen((value) async {
      n = value;
      debugPrint(value.toString());
    });
    expect(n, 0);
  });

  test(&quot;Fake async with timers&quot;, () {
    fakeAsync((fa) {
      int n = 0;
      var stream = Stream.fromIterable([1, 2, 3, 4, 5]);
      stream.listen((value) async {
        await Future.delayed(Duration(seconds: value));
        n = value;
        debugPrint(value.toString());
      });
      fa.flushTimers();
      expect(n, 5);
    });
  });

  test(&quot;Fake async with microtasks&quot;, () {
    fakeAsync((fa) {
      int n = 0;
      var stream = Stream.fromIterable([1, 2, 3, 4, 5]);
      stream.listen((value) async {
        n = value;
        debugPrint(value.toString());
      });
      fa.flushMicrotasks();
      expect(n, 5);
    });
  });
}

The first two tests show what we expect to happen in a synchronous unit test, like the one you did. n is not going to increase before expect is called. The second test is the most surprising, because even if our "async" task is actually synchronous (it just assigns a value and prints it), it only runs after the sync statements, including expect.

By using the fake_async package, you can manipulate the Event Queue and the Microtask Queue. Your unit test is nested into a fakeAsync call, and by calling flushTimers or flushMicrotasks you can complete all the async operations of the stream listener.

Notice that the fakeAsync tests would fail if you swapped the calls to flushTimers and flushMicrotasks, which shows that which of them you need to call depends on the concrete nature of the async tasks.

huangapple
  • 本文由 发表于 2023年4月13日 19:03:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/76004663.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定