Bloc: await UntilCalled doesn't stop the run after updating to 4.0.0

Created on 24 Apr 2020  Β·  4Comments  Β·  Source: felangel/bloc

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

  1. Works as expected before upgrading to 4.0.0 (Was using 3.2.0 previously).
  2. If I downgraded to 3.2.0 again, it worked.
  3. Because of that, I decided to post this here instead in mockito's repo.

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: flutter

community_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.0

dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^4.1.1
build_runner: ^1.7.3
json_serializable: ^3.2.5
gql_code_gen: ^0.1.5

flutter:
uses-material-design: true

question

Most helpful comment

Thanks Felix appreciate your answers

All 4 comments

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.)

  • If the answer is yes, then I'll make a sample app because there're some inconsistencies.
  • If your answer is no, then it worked as intended and it's just me who didn't truly get how 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

Was this page helpful?
0 / 5 - 0 ratings