felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.82k stars 3.39k forks source link

Bloc state does not change in testWidget even after adding it in mocktail when #2531

Closed georgek1991 closed 3 years ago

georgek1991 commented 3 years ago

Hi @felangel,

I am trying to test my ShortListPage page. I have added bloc_test and mocktail to the project dependencies. This is my code

ShortListPage.dart

class ShortListPage extends StatelessWidget {
  static Route route() {
    return MaterialPageRoute(builder: (_) => ShortListPage());
  }

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Short Link'),
        actions: [
                  Container(
                    margin: EdgeInsets.only(right: _padding),
                    child: InkWell(
                        child: RoundIcon(
                          text: context.read<AuthenticationBloc>().state.user?.fullName,
                        ),
                  ),
                ],
      ),
      body: SafeArea(
        child: BlocProvider<ShortListBloc>(
          create: (context) => ShortListBloc(
            ShortListRepository: ShortListRepository(
              service: Service(),
            ),
          )..add(ShortListRequested()),
          child: BlocBuilder<ShortListBloc, ShortListState>(
            builder: (builderContext, state) {
              if (state is ShortListFailure) {
                return _buildEmptyView(builderContext, state);
              } else if (state is ShortListSuccess) {
                if (state.ShortList.isEmpty) {
                  return _buildEmptyView(builderContext, state);
                }
                return _buildResultView(builderContext, state);
              }
              return _buildShimmerView(builderContext, state);
            },
          ),
        ),
      ),
    );
  }
}

ShortListPage_test.dart

class MockAuthenticationBloc
    extends MockBloc<AuthenticationEvent, AuthenticationState>
    implements AuthenticationBloc {}

class FakeAuthenticationEvent extends Fake implements AuthenticationEvent {}

class FakeAuthenticationState extends Fake implements AuthenticationState {}

class MockChangeThemeBloc extends MockBloc<ChangeThemeEvent, ChangeThemeState>
    implements ChangeThemeBloc {}

class FakeChangeThemeEvent extends Fake implements ChangeThemeEvent {}

class FakeChangeThemeState extends Fake implements ChangeThemeState {}

class MockShortListBloc extends MockBloc<ShortListEvent, ShortListState>
    implements ShortListBloc {}

class FakeShortListEvent extends Fake implements ShortListEvent {}

class FakeShortListState extends Fake implements ShortListState {}

void main() {
  late AuthenticationBloc mockAuthenticationBloc;
  late ChangeThemeBloc mockChangeThemeBloc;
  late ShortListBloc mockShortListBloc;

  setUpAll(() {
    registerFallbackValue<AuthenticationEvent>(FakeAuthenticationEvent());
    registerFallbackValue<AuthenticationState>(FakeAuthenticationState());

    registerFallbackValue<FakeChangeThemeEvent>(FakeChangeThemeEvent());
    registerFallbackValue<FakeChangeThemeState>(FakeChangeThemeState());

    registerFallbackValue<FakeShortListEvent>(FakeShortListEvent());
    registerFallbackValue<FakeShortListState>(FakeShortListState());
  });

  group('Home screen', () {
      setUp(() {
        mockAuthenticationBloc = MockAuthenticationBloc();
        mockChangeThemeBloc = MockChangeThemeBloc();
        mockShortListBloc = MockShortListBloc();

        when(() => mockChangeThemeBloc.state.themeData)
            .thenAnswer((_) => ChangeThemeState.lightTheme().themeData);
      });

      testWidgets('Renders correctly after failure',
          (WidgetTester tester) async {

          **//I am getting this mockUser1 value in the shortListPage**
        when(() => mockAuthenticationBloc.state)
            .thenAnswer((_) => AuthenticationAuthorized(user: mockUser1));

            **//I am NOT getting this state in ShortListPage. It is always the initial state of the ShortListBloc**
        when(() {
          return mockShortListBloc.state;
        }).thenReturn(ShortListFailure());
        await tester.pumpWidget(
          MultiBlocProvider(
            providers: [
              BlocProvider<AuthenticationBloc>.value(
                value: mockAuthenticationBloc,
              ),
              BlocProvider<ChangeThemeBloc>.value(
                value: mockChangeThemeBloc,
              ),
              BlocProvider<ShortListBloc>.value(
                value: mockShortListBloc,
              ),
            ],
            child: MaterialApp(
              home: Scaffold(
                body: ShortListPage(),
              ),
            ),
          ),
        );
        await tester.pump();
        ...
        expect(......)
        ...
      });
    });
}

main.dart

void main() {
  runApp(MultiBlocProvider(
    providers: [
      BlocProvider<AuthenticationBloc>(
        create: (context) => AuthenticationBloc(
          authenticationRepository: AuthenticationRepository(),
          signInRepository: SignInRepository(),
        )..add(AppStarted()),
      ),
      BlocProvider<ChangeThemeBloc>(
        create: (context) => ChangeThemeBloc(
          changeThemeRepository: ChangeThemeRepository(),
        ),
      ),
    ],
    child: App(),
  ));
}

The issue I am facing is that I am not able to mock the mockShortListBloc.state. Even though I am giving it inside the 'when' statement it does not change the state in my ShortListPage.

I also tried await tester.pumpAndSettle(); but as the state is not changing there is an infinite loading which causing timeout.

I am not sure where I am doing wrong.

Can you please help?

felangel commented 3 years ago

Hi @georgek1991 👋 Thanks for opening an issue!

I believe the problem is you're pumping a widget which already creates and provides the bloc internally. Your widget tree ends up looking something like:

- BlocProvider<MockShortListBloc>
  - MaterialApp
    - ShortListPage
      - BlocProvider<ShortListBloc>

The nearest provided bloc will always be used so you should decompose your ShortListPage into a page and view:

class ShortListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => ShortListBloc(),
      child: ShortListView(),
    );
  }
}

class ShortListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ShortListBloc, ShortListState>(...);
  }
}

This allows you to decouple the widget providing the bloc from the widget which consumes the bloc and makes it easy to inject a mock instance. In your tests you could then pump the ShortListView wrapped by a BlocProvider with a mock instance and it should work:

when(() => shortListBloc.state).thenReturn(...);
await tester.pumpWidget(
  BlocProvider.value(
    value: shortListBloc,
    child: ShortListView(),
  ),
);

Hope that helps 👍

felangel commented 3 years ago

@georgek1991 were you able to resolve the issue? If so can we close this issue? Thanks 🙏

georgek1991 commented 3 years ago

Yes @felangel

Thanks for the help