Bloc: Exception thrown on unit testing bloc: The following NoSuchMethodError was thrown building BlocBuilder<SignInBloc, SignInState>(dirty, state: _BlocBuilderBaseState<SignInBloc, SignInState>#2717f): The getter 'isFormValid' was called on null.

Created on 21 Jun 2020  ยท  10Comments  ยท  Source: felangel/bloc

Describe the bug
I was following the Firebase login tutorial and I ended with this issue while adding unit tests to my code (actual code works fine)

To Reproduce
Steps to reproduce the behavior:

  1. My SignOn Screen code (I am using getIt for dependency injection)
class SignInScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: Text('Sign In'),
        backgroundColor: Colors.redAccent,
      ),
      body: BlocProvider<SignInBloc>(
        create: (context) => getIt<SignInBloc>(),
        child: SignInForm(),
      ),
    );
  }
}
  1. The SignInForm is exactly like the LoginForm code provided in the tutorial

  2. I created Mock classes for unit testing the SignInScreen like this

@test
@Injectable(as: SignInBloc)
class MockSignInBloc extends MockBloc<SignInEvent, SignInState>
    implements SignInBloc {}

(The injection works fine I believe)

  1. I get this error on unit testing the SignInScreen
    โ•โ•โ•ก EXCEPTION CAUGHT BY WIDGETS LIBRARY โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
    The following NoSuchMethodError was thrown building BlocBuilder(dirty,
    state: _BlocBuilderBaseState#8d82e):
    The getter 'isFormValid' was called on null.
    Receiver: null
    Tried calling: isFormValid

Expected behavior
The unit tests should work fine with the initial SignInState state emitted from the bloc

Any reason why the initial state is not emitted by the mocked bloc resulting in the state being null?

bloc_test question

Most helpful comment

@felangel You have gone above and beyond for this refactor! Thanks for putting in the time to answer my question and also making the existing code better. I feel indebted :).

Once again thank you!!

All 10 comments

Hi @geovin89 ๐Ÿ‘‹
Thanks for opening an issue!

Since you're mocking the bloc you need to stub the state otherwise the bloc's state will be null.

when(signInBloc.state).thenReturn(SomeState());
// The rest of the test...

Hope that helps ๐Ÿ‘

Thanks a lot for the quick response felangel

One more question, so how does the

blocTest(
      'emits [] when nothing is added',
      build: () async => CounterBloc(),
      skip: 0,
      expect: [InitialState],
    );

work without mocking returning states?

Additonally is there anyway blocTests can be for adding multiple events? like this

blocTest('emits states from initial when email is valid',
          build: () async {
            return SignInBloc(signInWithCredentials);
          },
          act: (signInBloc) => signInBloc.add(
                SignInEmailChanged(email: invalidEmail),
                SignInEmailChanged(email: validEmail),
              ),
          wait: const Duration(milliseconds: 300),
          expect: [emailInvalidState, emailValidState]);

Thanks in advance

blocTest should be used to test your bloc (the real implementation) so you shouldnโ€™t be using a MockBloc in your blocTest. Blocs should be mocked in widget tests to ensure that you can fully test how the widget behaves in each scenario. With any tests, the thing that your testing should be the actual implementation and all of the dependencies should ideally be mocked.

Regarding your second question, you can just call add multiple times or chain them together like:

blocTest('emits states from initial when email is valid',
          build: () async {
            return SignInBloc(signInWithCredentials);
          },
          act: (signInBloc) => 
              signInBloc
                ..add(SignInEmailChanged(email: invalidEmail))
                ..add(SignInEmailChanged(email: validEmail),)
              ),
          wait: const Duration(milliseconds: 300),
          expect: [emailInvalidState, emailValidState]);

Closing for now but feel free to comment with additional questions and Iโ€™m happy to continue the conversation ๐Ÿ‘

Gotcha!

Thanks a lot Felix. Hopefully the last question :). This for my understanding of how whenListen works.
I was trying to widget test snack bar display in the LoginForm screen
```dart
testWidgets('should show snack bar loading when bloc returns loading state',
(WidgetTester tester) async {
whenListen(signInBloc,
Stream.fromIterable([SignInState.initial(), SignInState.loading()]));

  await tester.pumpWidget(
    BlocProvider.value(
      value: signInBloc,
      child: MaterialApp(
        home: Scaffold(body: SignInForm()),
      ),
    ),
  );

  await tester.pump();

  final snackBarFinder = find.byKey(snackBarKey);
  final widget = snackBarFinder.evaluate().first.widget as SnackBar;
   expect(snackBarFinder, findsOneWidget);

});

````
if I pass only SignInState.loading(), the test fails; I just want to know why.

P.S. Is there a better way for testing how the UI would behave when bloc receives each of the events? Like first you added emailChanged and then passwordChanged, Submit etc
kind of like this
```dart
await tester.enterText(find.byKey(emailField), '[email protected]');
await tester.pumpAndSettle();

await tester.enterText(find.byKey(passwordField), '');
await tester.pumpAndSettle();

await tester.tap(find.byKey(loginButton));
await tester.pumpAndSettle();
.
.
.
//test for widgets..
````

I did try your suggestion

final emailValidState = SignInState.initial();
final emailInvalidState = emailValidState.copyWith(isEmailValid: false);

blocTest(
        'should emit correct states when email is changed from invalid to valid',
        build: () async {
          return SignInBloc(signInWithCredentials);
        },
        act: (signInBloc) => signInBloc
          ..add(SignInEmailChanged(email: invalidEmail))
          ..add(SignInEmailChanged(email: validEmail)),
        wait: const Duration(milliseconds: 300),
        verify: (_) async {
          verifyNever(signInWithCredentials(any));
        },
        expect: [emailInvalidState, emailValidState],
      );

but I am receiving this error

.<fn>.<fn>
package:bloc_test/src/bloc_test.dart 140:20                                 blocTest.<fn>.<fn>
===== asynchronous gap ===========================
dart:async                                                                  _asyncThenWrapperHelper
package:bloc_test/src/bloc_test.dart                                        blocTest.<fn>.<fn>
dart:async                                                                  runZoned
package:bloc_test/src/bloc_test.dart 135:11                                 blocTest.<fn>

type 'SignInBloc' is not a subtype of type 'Future<void>'

if I change the code to

act: (signInBloc) async => await signInBloc
          ..add(SignInEmailChanged(email: invalidEmail))
          ..add(SignInEmailChanged(email: validEmail)),

it works fine without the debounce stream (and you dont need await either) but fails with no events with the debouncestream.

@geovin89 can you please share a link to a sample app which reproduces the issue and I'll take a look and try to open a pull request ๐Ÿ‘

I have created a sample app here https://github.com/geovin89/flutterbloctestapp. I have posted my questions in there as 3 Todos so that you dont get lost in there

@geovin89 sorry for the delay! I opened a pull request with my suggestions ๐Ÿ‘

@felangel You have gone above and beyond for this refactor! Thanks for putting in the time to answer my question and also making the existing code better. I feel indebted :).

Once again thank you!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tigranhov picture tigranhov  ยท  3Comments

clicksocial picture clicksocial  ยท  3Comments

nhwilly picture nhwilly  ยท  3Comments

shawnchan2014 picture shawnchan2014  ยท  3Comments

Reidond picture Reidond  ยท  3Comments