英文:
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<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?
答案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 调用中,通过调用 flushTimers
或 flushMicrotasks
,你可以完成流监听器的所有异步操作。
请注意,如果你交换了对 flushTimers
和 flushMicrotasks
的调用,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 '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);
});
});
}
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论