Describe the bug
await UntilCalled doesn't stop a function run even after it's called. Works in ^3.2.0.
Expected behavior
await UntilCalled stops a function run and continuing the test code, instead of continuing the function run.
Code
1. store_form_bloc.dart
// imports omitted
class StoreFormBloc extends Bloc<StoreFormEvent, StoreFormState> {
final cs.CreateStore createStore;
final us.UpdateStore updateStore;
final vsf.ValidateStoreForm validation;
StoreFormBloc(
{@required this.createStore,
@required this.updateStore,
@required this.validation});
@override
StoreFormState get initialState => StoreFormInitialState();
@override
Stream<StoreFormState> mapEventToState(StoreFormEvent event) async* {
if (event is CreateStoreEvent) {
yield* _handleCreateStoreEvent(event);
} else if (event is UpdateStoreEvent) {
yield* _handleUpdateStoreEvent(event);
}
}
Stream<StoreFormState> _handleCreateStoreEvent(
CreateStoreEvent event) async* {
yield StoreFormLoadingState();
final validationResult = validation(vsf.Params(
code: event.code,
name: event.name,
tokenMultiplier: event.tokenMultiplier,
luckyDrawMultiplier: event.luckyDrawMultiplier,
));
yield* validationResult.fold(
(failure) async* {
yield _$mapFailureToError(failure);
},
(_) async* {
final result = await createStore(cs.Params(
code: event.code,
name: event.name,
tokenMultiplier: double.tryParse(event.tokenMultiplier),
luckyDrawMultiplier: double.tryParse(event.luckyDrawMultiplier),
));
yield* result.fold(
(failure) async* {
yield _$mapFailureToError(failure);
},
(store) async* {
yield StoreFormCreatedSuccessfullyState(
message: '${event.name} berhasil didaftarkan di ${event.code}!',
);
},
);
},
);
}
Stream<StoreFormState> _handleUpdateStoreEvent(
UpdateStoreEvent event) async* {
yield StoreFormLoadingState();
final validationResult = validation(vsf.Params(
id: event.id,
code: event.code,
name: event.name,
tokenMultiplier: event.tokenMultiplier,
luckyDrawMultiplier: event.luckyDrawMultiplier,
isRenting: event.isRenting,
));
yield* validationResult.fold(
(failure) async* {
yield _$mapFailureToError(failure);
},
(_) async* {
final result = await updateStore(us.Params(
id: event.id,
code: event.code,
name: event.name,
tokenMultiplier: double.tryParse(event.tokenMultiplier),
luckyDrawMultiplier: double.tryParse(event.luckyDrawMultiplier),
isRenting: event.isRenting,
));
yield* result.fold(
(failure) async* {
yield _$mapFailureToError(failure);
},
(store) async* {
yield StoreFormUpdatedSuccessfullyState(
message: '${event.name} berhasil dirubah di ${event.code}!',
);
},
);
},
);
}
}
2. create_store_event_test.dart
// imports omitted
class MockCreateStore extends Mock implements cs.CreateStore {}
class MockUpdateStore extends Mock implements us.UpdateStore {}
class MockValidateStoreForm extends Mock implements vsf.ValidateStoreForm {}
void main() {
StoreFormBloc bloc;
MockCreateStore mockCreateStore;
MockUpdateStore mockUpdateStore;
MockValidateStoreForm mockValidate;
Map<String, dynamic> storeFixture;
Store store;
setUp(() {
mockCreateStore = MockCreateStore();
mockUpdateStore = MockUpdateStore();
mockValidate = MockValidateStoreForm();
bloc = StoreFormBloc(
createStore: mockCreateStore,
updateStore: mockUpdateStore,
validation: mockValidate,
);
storeFixture = Map<String, dynamic>.from(
json.decode(fixture('fixtures/stores/valid.json')));
store = Store.fromJson(storeFixture);
});
tearDown(() {
bloc?.close();
});
final codeFixture = 'XYZ';
final nameFixture = 'AnyName';
final tokenMultiplierFixture = '1.2';
final luckyDrawMultiplierFixture = '1.2';
void setUpSuccessfulStoreFormValidation() {
when(mockValidate(any)).thenReturn(Right(true));
}
void setUpSuccessfulCreateStore() {
when(mockCreateStore(any)).thenAnswer((_) async => Right(store));
}
test('should call validateStoreForm', () async {
setUpSuccessfulStoreFormValidation();
setUpSuccessfulCreateStore(); // Note: In 3.2.0, this line wasn't needed, since it won't reaches mocked code anyway.
bloc.add(CreateStoreEvent(
code: codeFixture,
name: nameFixture,
tokenMultiplier: tokenMultiplierFixture,
luckyDrawMultiplier: luckyDrawMultiplierFixture,
));
await untilCalled(mockValidate(any));
verify(mockValidate(vsf.Params(
code: codeFixture,
name: nameFixture,
tokenMultiplier: tokenMultiplierFixture,
luckyDrawMultiplier: luckyDrawMultiplierFixture,
)));
});
}
*Logs *
1. flutter analyze
_Taking a long time to analyze, will update this post once finished._
2. flutter doctor -v
[β] Flutter (Channel stable, v1.12.13+hotfix.9, on Mac OS X 10.15.4 19E287,
locale en-ID)
β’ Flutter version 1.12.13+hotfix.9 at
/Users/[myname]/Development/flutter
β’ Framework revision f139b11009 (4 weeks ago), 2020-03-30 13:57:30 -0700
β’ Engine revision af51afceb8
β’ Dart version 2.7.2[β] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
β’ Android SDK at /Users/[myname]/Development/Android/sdk
β’ Android NDK location not configured (optional; useful for native profiling support)
β’ Platform android-29, build-tools 29.0.2
β’ ANDROID_HOME = /Users/[myname]/Development/Android/sdk
β’ Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
β’ Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)
β’ All Android licenses accepted.[β] Xcode - develop for iOS and macOS (Xcode 11.4.1)
β’ Xcode at /Applications/Xcode.app/Contents/Developer
β’ Xcode 11.4.1, Build version 11E503a
β’ CocoaPods version 1.9.0[!] Android Studio (version 3.5)
β’ Android Studio at /Applications/Android Studio.app/Contents
β Flutter plugin not installed; this adds Flutter specific functionality.
β Dart plugin not installed; this adds Dart specific functionality.
β’ Java version OpenJDK Runtime Environment (build 1.8.0_202-release-1483-b49-5587405)[β] VS Code (version 1.44.0)
β’ VS Code at /Applications/Visual Studio Code.app/Contents
β’ Flutter extension version 3.9.1[!] Connected device
! No devices available! Doctor found issues in 2 categories.
3. Test fail message
Unhandled error NoSuchMethodError: The method 'fold' was called on null.
Receiver: null
Tried calling: fold>(Closure: (Failure) => Stream , Closure: (Store) => Stream ) occurred in bloc Instance of 'StoreFormBloc'. 0 Object.noSuchMethod (dart:core-patch/object_patch.dart:53:5)
1 StoreFormBloc._handleCreateStoreEvent.
(package:[projectname]/features/manage_store/presentation/bloc/store_form_bloc/store_form_bloc.dart:63:23)
2 Right.fold (package:dartz/src/either.dart:92:64)
3 StoreFormBloc._handleCreateStoreEvent (package:[projectname]/features/manage_store/presentation/bloc/store_form_bloc/store_form_bloc.dart:51:29)
4 StoreFormBloc.mapEventToState (package:[projectname]/features/manage_store/presentation/bloc/store_form_bloc/store_form_bloc.dart:34:14)
5 Bloc._bindEventsToStates.
(package:bloc/src/bloc.dart:252:20) 6 Stream.asyncExpand.onListen.
(dart:async/stream.dart:576:30) 7 StackZoneSpecification._registerUnaryCallback.
. (package:stack_trace/src/stack_zone_specification.dart:129:26) 8 StackZoneSpecification._run (package:stack_trace/src/stack_zone_specification.dart:209:15)
9 StackZoneSpecification._registerUnaryCallback.
(package:stack_trace/src/stack_zone_specification.dart:129:14) 10 _rootRunUnary (dart:async/zone.dart:1134:38)
11 _CustomZone.runUnary (dart:async/zone.dart:1031:19)
12 _CustomZone.runUnaryGuarded (dart:async/zone.dart:933:7)
13 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:338:11)
14 _DelayedData.perform (dart:async/stream_impl.dart:593:14)
15 _StreamImplEvents.handleNext (dart:async/stream_impl.dart:7
Additional context
package.yml
name: projectname
description: A new Flutter project.version: 1.0.0+1
environment:
sdk: ">=2.3.0 <3.0.0"dependencies:
flutter:
sdk: fluttercommunity_material_icon: 3.5.95
cupertino_icons: ^0.1.2
google_fonts: 0.3.2
get_it: ^3.0.3
equatable: ^1.1.1
dartz: ^0.8.9
http: ^0.12.0+2
meta: ^1.1.8
json_annotation: ^3.0.1
google_sign_in: ^4.1.1
firebase_core: ^0.4.4
firebase_auth: ^0.15.4
firebase_messaging: ^6.0.9
firebase_analytics: ^5.0.11
graphql: ^3.0.0
flutter_bloc: ^4.0.0 // Note: Test passing normally in ^3.2.0
qr_flutter: ^3.1.0
jaguar_jwt: ^2.1.6
barcode_scan: any
dio: ^3.0.9
image_cropper: ^1.1.2
image_picker: ^0.6.3+4
intl: ^0.16.1
stream_transform: ^1.2.0dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^4.1.1
build_runner: ^1.7.3
json_serializable: ^3.2.5
gql_code_gen: ^0.1.5flutter:
uses-material-design: true
Hi @moseskarunia π
Thanks for opening an issue!
Would it be possible for you to put together a simple app which reproduces this issue and send me the link? It would be really helpful if I can reproduce/debug this issue locally, thanks! π
If I had to guess, the reason the test is failing now is because in 4.0.0 if uncaught exceptions occur in a bloc they are bubbled up (in debug builds) whereas previously they were swallowed. I'm guessing you aren't properly stubbing either the createStore or validation calls within the bloc and they are returning a null response which causes fold to be called on null. I think this was just a bug in your test setup which wasn't caught in v3.2.0 but is caught in 4.0.0.
Let me know if that helps and if you're still having trouble it would be great if you could share a complete sample app which I can run locally, thanks π
Thanks Felix for the enlightenment. Turns out you are correct. It was also a problem in 3.2.0, I tried to debug it with debug instead of run and flutter. And I can reproduce the same error.
So, after your previous comment, I did some more research about the behaviour of untilCalled. Turns out I found untilCalled didn't stop the function execution (?)
Should untilCalled normally stop the function execution right after the call to stubbed function inside it? (I mean, like similar to calling "return;" after stubbed validation is called.)
untilCalled really do its job.Thanks Felix. Really appreciate the help.
No problem! I donβt believe untilCalled will stop execution after the function. It just allows you to wait until the stubbed method was called. Hope that helps π
Thanks Felix appreciate your answers
Most helpful comment
Thanks Felix appreciate your answers