-
Notifications
You must be signed in to change notification settings - Fork 271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unexpected behaviour of BehaviorSubject under FakeAsync #395
Comments
Using the fakeAsync as following : |
But I am still unable to get it working using
|
Okay, theirs more . Even using
Which basically means suggestion in this comment is not working either. Also, the following test, i.e. adding
Maybe #365 should be opened again and this be closed, being a duplicate ? |
This is becoming more troublesome. This test case, although prints the stack-trace, passes. It should actually fail.
|
I believe this behavior caused by Dart itself. Here If See the following issues for more information: I researched this because I have a similar problem in my tests, but with import 'dart:async';
import 'package:quiver/testing/async.dart';
import 'package:rxdart/rxdart.dart';
import 'package:test/test.dart';
void main() {
test('Counter should be incremented', () {
FakeAsync().run((fakeAsync) {
var counter = 0;
final _controller = StreamController<void>();
Observable(_controller.stream)
.debounceTime(const Duration(minutes: 1))
.listen((_) {
print('increment');
counter += 1;
});
_controller.add(null);
fakeAsync.flushTimers();
expect(counter, 1);
});
});
} This test fails with
I still don't have a proper workaround. It seems that the cheapest way is to change onCancel: () => subscription.cancel() to onCancel: () => Future.value(subscription.cancel()) This will override the zone of the Still, I hope that somebody from the RxDart team will help us to find a good workaround. |
Me too. Waiting for any updates. |
Hi all -- thanks so much to @dmitryelagin -- that is an amazing investigation. I'll be honest, we've tried to use and make FakeAsync work in the past, but it does some odd things that have led me to really question whether it should ever be used to test Streams, since you can't guarantee the behavior under test will be the behavior you get when the app is actually running. However, since Flutter uses FakeAsync everywhere, I think we need to support it. Overall, this is a bigger change that will requires a lot of additional tests. I'll try to find some time this week or early next to go through and add FakeAsync tests for, well everything, to ensure using a custom future instead the constant nullFuture fixes everything up! Thanks for the report and the great investigation. |
Update: I've taken some time to do a prototype of this change. However, when I think about it a bit more, making the suggested change fixes the bugs under FakeAsync conditions, but IMO would actually be a bad change under "normal" conditions. The core problem: By returning a if (_cancelFuture != null &&
!identical(_cancelFuture, Future._nullFuture)) {
_cancelFuture.whenComplete(sendDone);
} This means that if the inner Another issue: RxDart isn't the only library affected by this problem. For example, regular old Therefore, it feels like while we could make this change to RxDart, it's actually not a good change, since it would fix the issues for a rather unique, heavily modified Test environment at the cost of risking unexpected behavior in the "normal" production environments. I think the proper solution is to request more help from the Dart and Quiver teams on how best to resolve this issue. |
I found the following solution/workaround for the problem. The problem is that when you have some asynchronous task with both microtasks that are registered in the fake async zone and microtasks that are registered in the root zone (because of the A function creating a test case under fake async could then look like: @isTest
void testUnderFakeAsyncFixed(String description, Function() body,
{Timeout timeout}) {
test(description, () async {
var fakeAsync = FakeAsync();
var f = fakeAsync.run((async) {
return body();
});
var isDone = false;
unawaited(f.whenComplete(
() => isDone = true)); // check if all work in body has been done
while (!isDone) {
// flush the microtasks in real async zone
await Future.microtask(() => null);
// flush the microtasks in the fake async zone
fakeAsync.flushMicrotasks();
}
return f;
}, timeout: Timeout(Duration(seconds: 1)));
}
A fix for @isTest
void testWidgetsFixed(
String description, Future<void> Function(WidgetTester) body,
{Timeout timeout}) {
testWidgets(description, (tester) async {
var f = body(tester);
var isDone = false;
unawaited(f.whenComplete(() => isDone = true));
// while not done register a microtask in the real async zone
while (!isDone) {
await tester.runAsync(() => Future.microtask(() => null));
}
return f;
}, timeout: timeout);
}
A full example of this with some tests: import 'dart:async';
import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart';
import 'package:pedantic/pedantic.dart';
void main() {
Future Function() createFuture;
Future<void> methodUnderTest() async {
await Future.microtask(
() => null); // creates a microtask in fake async zone
await createFuture(); // creates a microtask in real async zone
await Future.microtask(() => null);
await createFuture();
await Future.microtask(() => null);
}
group('StreamSubscription cancel on root Zone', () {
createFuture = () {
return StreamController()
.stream
.listen((value) {})
.cancel(); // cancel returns the static value Future._nullFuture
};
test('under real async', methodUnderTest); // this succeeds
testUnderFakeAsync('under fake async', methodUnderTest,
timeout: Timeout(Duration(seconds: 1))); // this times out
testUnderFakeAsyncFixed('under fake async', methodUnderTest,
timeout: Timeout(Duration(seconds: 1))); // this succeeds
testWidgets('under testWidgets', (tester) => methodUnderTest(),
timeout: Timeout(Duration(seconds: 1))); // this times out
testWidgetsFixed('under testWidgets fixed', (tester) => methodUnderTest(),
timeout: Timeout(Duration(seconds: 1))); // this succeeds
});
group('Future from root zone', () {
createFuture = () {
return Zone.root.run(() => Future.value());
};
test('under real async', methodUnderTest); // this succeeds
testUnderFakeAsync('under fake async', methodUnderTest,
timeout: Timeout(Duration(seconds: 1))); // this times out
testUnderFakeAsyncFixed('under fake async', methodUnderTest,
timeout: Timeout(Duration(seconds: 1))); // this succeeds
testWidgets('under testWidgets', (tester) => methodUnderTest(),
timeout: Timeout(Duration(seconds: 1))); // this times out
testWidgetsFixed('under testWidgets fixed', (tester) => methodUnderTest(),
timeout: Timeout(Duration(seconds: 1))); // this succeeds
});
}
@isTest
void testUnderFakeAsync(String description, Function() body,
{Timeout timeout}) {
test(description, () async {
var fakeAsync = FakeAsync();
var f = fakeAsync.run((async) {
return body();
});
// flush the microtasks created in the fake async zone
// this blocks on first microtask in real async zone
fakeAsync.flushMicrotasks();
return f;
}, timeout: Timeout(Duration(seconds: 1)));
} |
It seems most of the time, this issue is caused by the Although the cause is not within the Similarly, the streams returned/created in this package could be wrapped, so that the cancel method on the subscriptions do not return the If you want, I can try to implement these changes and do a pull request, but I am not sure that you will accept them. So, before spending time on it, I'd like to know if you would accept such an approach. |
The following test with
BehaviourSubject
fails while the one withStreamController
works fine. I have tried this with rxDart versions0.22.4
,0.22.5
and0.23.1
. None of them passes.The text was updated successfully, but these errors were encountered: