Skip to content
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

Help: Flutter Bloc Test: Unexpected State Change After Multiple Events #4044

Closed
dipu0 opened this issue Jan 15, 2024 · 6 comments
Closed

Help: Flutter Bloc Test: Unexpected State Change After Multiple Events #4044

dipu0 opened this issue Jan 15, 2024 · 6 comments
Assignees
Labels
question Further information is requested

Comments

@dipu0
Copy link

dipu0 commented Jan 15, 2024

I'm currently facing an issue with Flutter Bloc testing where I have a sequence of events: fetching items, then decrementing the item count. The problem is that the expected state after the decrement is not matching the actual state. It seems like the item count is being decremented from the initial state instead of the state after fetching the items.

In my case, I have a single state class which uses copyWith function. In state response list remains same from GetCartItem to decrement in the bloc.stream getting data. When I call two events one after another, like 'get items' event then than 'decremented count'.
Here's the relevant code snippet:

    class MockRepository extends Mock implements Repository {}
    
    void main() {
      late MockRepository mockRepository;
    
      setUp(() {
        mockRepository = MockRepository();
      });
      
        blocTest<TestBloc, ItemState>(
        'emits correct states when decrementing the cart item count and updating cart items',
        setUp: () {
          when(() => mockRepository.getCartItemList()).thenAnswer(
                (_) async => [
              ItemEntity(
                id: 1,
                productName: 'Product 1',
                productPrice: '10.00',
                cartItemCount: 2,
              ),
            ],
          );
          when(() => mockRepository.updateCartItem(any())).thenAnswer((_) async => true);
        },
        build: () => TestBloc(repository: mockRepository),
        act: (bloc) {
          bloc.add(GetCartItemListEvent());
          bloc.add(DecrementCartItemEvent(1));
        },
        expect: () => [
          ItemState(status:  CallbackStatus.loading,total: 0.0),
          ItemState(status: CallbackStatus.success,
              response:[ItemEntity(
                id: 1,
                productName: 'Product 1',
                productPrice: '10.00',
                cartItemCount: 2,
              )],total: 20.0),
          ItemState(status: CallbackStatus.initial,
              response:[ItemEntity(
                id: 1,
                productName: 'Product 1',
                productPrice: '10.00',
                cartItemCount: 2,
              )],total: 20.0),
          ItemState(status: CallbackStatus.loading,
              response: [
                ItemEntity(
                  id: 1,
                  productName: 'Product 1',
                  productPrice: '10.00',
                  cartItemCount: 2,
                ),
              ], total: 20.0),
          ItemState(status: CallbackStatus.initial, response: [
            ItemEntity(
              id: 1,
              productName: 'Product 1',
              productPrice: '10.00',
              cartItemCount: 1,
            )
          ], total: 10.0),
        ],
      );
      
    }

Error shows


    Expected: [
    ItemState:ItemState(CallbackStatus.loading, null, 0.0),
    
            ItemState:ItemState(CallbackStatus.success, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 2}], 20.0),
    
            ItemState:ItemState(CallbackStatus.initial, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 2}], 20.0),
    
            ItemState:ItemState(CallbackStatus.loading, [ItemEntity{id: 1, productName: Product 1,productPrice: 10.00, cartItemCount: 2}], 20.0),
    
            ItemState:ItemState(CallbackStatus.initial, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 1}], 10.0)
          ]
    
    Actual: [
    ItemState:ItemState(CallbackStatus.loading, null, 0.0),
    
            ItemState:ItemState(CallbackStatus.success, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 1}], 20.0),
            ItemState:ItemState(CallbackStatus.initial, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 1}], 20.0),
            ItemState:ItemState(CallbackStatus.loading, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 1}], 20.0),
            ItemState:ItemState(CallbackStatus.initial, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 1}], 10.0)
          ]
    
    Which: at location [1] is ItemState:<ItemState(CallbackStatus.success, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 1}], 20.0)> instead of ItemState:<ItemState(CallbackStatus.success, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 2}], 20.0)>
==== diff ========================================
    [ItemState(CallbackStatus.loading, null, 0.0),
    ItemState(CallbackStatus.success, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: [-2-]{+1+}}], 20.0),
    ItemState(CallbackStatus.initial, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: [-2-]{+1+}}], 20.0),
    ItemState(CallbackStatus.loading, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: [-2-]{+1+}}], 20.0),
    ItemState(CallbackStatus.initial, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 1}], 10.0)]
==== end diff =================================================================

The issue is that the actual state after the second event shows the item count decremented from the initial state, but it should be from the state after fetching items.

I would appreciate any insights or suggestions on how to resolve this issue. Thanks in advance!

@dipu0 dipu0 added the bug Something isn't working label Jan 15, 2024
@dipu0 dipu0 changed the title fix: Flutter Bloc Test: Unexpected State Change After Multiple Events Help: Flutter Bloc Test: Unexpected State Change After Multiple Events Jan 15, 2024
@elianortega
Copy link
Contributor

Hello @dipu0 can you share the TestBloc code?

@dipu0
Copy link
Author

dipu0 commented Jan 17, 2024

Hello @dipu0 can you share the TestBloc code?
@elianortega hello here is my bloc code

class TestBloc extends Bloc<CartEvent, ItemState> {
  final Repository? repository;

  TestBloc({this.repository}) : super(ItemState()) {
    on<GetCartItemListEvent>(_getCartItemList);
    on<IncrementCartItemEvent>(_itemIncrease);
    on<DecrementCartItemEvent>(_itemDecrease);
  }

  void _getCartItemList(
      GetCartItemListEvent event, Emitter<ItemState> emit) async {
    emit(state.copyWith(status: CallbackStatus.loading, total: 0.0));
    try {
      List<ItemEntity?>? response = await repository!.getCartItemList();
      double total = 0;
      response!.forEach((element) {
        total = total + (double.parse(element!.productPrice!) * element.cartItemCount!);
      });
      emit(state.copyWith(
          status: CallbackStatus.success, response: response, total: total));
    } catch (e) {
      emit(state.copyWith(status: CallbackStatus.error));
    }
    emit(state.copyWith(status: CallbackStatus.initial));
  }

  void _itemIncrease(
      IncrementCartItemEvent event, Emitter<ItemState> emit) async {
    try{
      emit(state.copyWith(status: CallbackStatus.loading));
      List<ItemEntity> newCartList = [];
      double? total;
      for (int i = 0; i < state.response!.length; i++) {
        if (state.response![i]!.id == event.productId &&
            state.response![i]!.cartItemCount! < 99) {
          state.response![i]!.cartItemCount =
              state.response![i]!.cartItemCount! + 1;
          total =
              state.total! + double.parse(state.response![i]!.productPrice!);
        }
        newCartList.add(state.response![i]!);
      }
      bool? isAdded = await repository!.updateCartItem(newCartList);

      if(isAdded != null && isAdded == true){
        emit(state.copyWith(
            status: CallbackStatus.initial, response: newCartList, total: total));
      }
    }catch(e){
      emit(state.copyWith(status: CallbackStatus.initial));
    }
  }

  void _itemDecrease(
      DecrementCartItemEvent event, Emitter<ItemState> emit) async {
    try{
      emit(state.copyWith(status: CallbackStatus.loading));
      List<ItemEntity> newCartList = [];
      double? total;
      for (int i = 0; i < state.response!.length; i++) {
        if (state.response![i]!.id == event.productId &&
            state.response![i]!.cartItemCount! > 1) {
          state.response![i]!.cartItemCount = state.response![i]!.cartItemCount! - 1;
          total =
              state.total! - double.parse(state.response![i]!.productPrice!);
        }
        newCartList.add(state.response![i]!);
      }

      bool? isAdded = await repository!.updateCartItem(newCartList);
      if(isAdded != null && isAdded == true){
        emit(state.copyWith(
            status: CallbackStatus.initial, response: newCartList, total: total));
      }

    }catch(e){
      emit(state.copyWith(status: CallbackStatus.initial));
    }

  }
}

@felangel felangel added question Further information is requested and removed bug Something isn't working labels Jan 18, 2024
@elianortega
Copy link
Contributor

Hello @dipu0, is very hard to debug without syntax highlighting or running the code. But from a quick overview, I think something that could simplify your test is instead of adding the GetCartItemListEvent event to initialize the data before doing the actual test of DecrementCartItemEvent .

You can use the seed property from bloc test docs here.

seed is an optional Function that returns a state which will be used to seed the bloc before act is called.

Doing this will help you isolate the test scenario and analyze what is going on.

@dipu0
Copy link
Author

dipu0 commented Jan 20, 2024

Hello @dipu0, is very hard to debug without syntax highlighting or running the code. But from a quick overview, I think something that could simplify your test is instead of adding the GetCartItemListEvent event to initialize the data before doing the actual test of DecrementCartItemEvent .

You can use the seed property from bloc test docs here.

seed is an optional Function that returns a state which will be used to seed the bloc before act is called.

Doing this will help you isolate the test scenario and analyze what is going on.

Already tried to use seed, but same!!!


class MockRepository extends Mock implements Repository {}

void main() {
  late MockRepository mockRepository;

  setUp(() {
    mockRepository = MockRepository();
    when(() => mockRepository.getCartItemList()).thenAnswer(
          (_) async => [
        ItemState(
          id: 1,
          productName: 'Product 1',
          productPrice: '10.00',
          cartItemCount: 2,
        ),
      ],
    );
    when(() => mockRepository.updateCartItem(any())).thenAnswer((_) async => true);
  });

  blocTest<TestBloc, ItemState>(
    'emits correct states when decrementing the cart item count and updating cart items',
    build: () => TestBloc(repository: mockRepository),
    act: (bloc) async {
      // Dispatch the GetCartItemListEvent to populate the initial state
      bloc.add(GetCartItemListEvent());

      // Wait for the GetCartItemListEvent to complete
      await Future.delayed(Duration(milliseconds: 200)); // Adjust the delay as needed

      // Verify the initial state after fetching cart items
      print("Initial State: ${bloc.state}");

      // Dispatch the DecrementCartItemEvent
      bloc.add(DecrementCartItemEvent(1));

      // Wait for the state to be emitted
      await expectLater(
        bloc.stream,
        emitsInOrder([
          ItemState(
            status: CallbackStatus.loading,
            response: [
              ItemState(
                id: 1,
                productName: 'Product 1',
                productPrice: '10.00',
                cartItemCount: 1,
              ),
            ],
            total: 20.0,
          ),
          ItemState(
            status: CallbackStatus.initial,
            response: [
              ItemState(
                id: 1,
                productName: 'Product 1',
                productPrice: '10.00',
                cartItemCount: 1, // Count should effet on this but it changers on previous cartItemCount too
              ),
            ],
            total: 10.0,
          ),
        ]),
      );
    },
  );
}

If I set the status to CallbackStatus.loading and itemCartCount: 2, it produces an error. If I set it to 1, the test case passes.

Expected: should do the following in order:
          • emit an event that
ItemState:<ItemState(CallbackStatus.loading, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 2}], 20.0)>

          • emit an event that
ItemState:<ItemState(CallbackStatus.initial, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 1}], 10.0)>

  Actual: <Instance of '_BroadcastStream<ItemState>'>

   Which: emitted 
• ItemState(CallbackStatus.loading, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 1}], 20.0)

• ItemState(CallbackStatus.initial, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 1}], 10.0)

which didn't emit an event that
ItemState:<ItemState(CallbackStatus.loading, [ItemEntity{id: 1, productName: Product 1, productPrice: 10.00, cartItemCount: 2}], 20.0)>

Here, and also in the previous test, the main problem was in the response (ItemEntity List). In the state, I have three things: CallbackStatus, a response which is a list of ItemEntity, and total. In every stage callback and total are correctly emitted, but in the response list, cartItemCount should start with 2 while CallbackStatus is loading and decrease by 1 in the last emit. However, cartItemCount stays the same as the last emit from the initial emits.

@elianortega
Copy link
Contributor

elianortega commented Jan 20, 2024

@dipu0 In that case, this is an issue probably with your implementation, as I said is very hard to analyze or debug with just the code snippets, can you please share a link to a minimal reproduction sample on GitHub or DartPad? It would be much easier to help if I can reproduce the issue locally, thanks!

@felangel
Copy link
Owner

felangel commented Feb 1, 2024

Closing for now since there aren't any actionable next steps without a link to a minimal reproduction sample. If this is still an issue please provide a link to a minimal reproduction sample on either GitHub or DartPad and I'm happy to take a closer look, thanks!

@felangel felangel closed this as completed Feb 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants