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:
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(),
),
);
}
}
The SignInForm is exactly like the LoginForm code provided in the tutorial
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)
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?
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!!
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!!