Flutter Mockito Test StreamSubscription onError - flutter

I am trying to test a cubit which subscribes to a firebaseAuth listener that is prone to fail and presumably call "onError" on the subscriber.
I am struggling to mock the exception as a subscription does not really "throw" per se, but rather calls onError using a callback.
My cubit defines:
configureUserListener(String uid) {
userSubscription = _listenToUser(uid).listen((user) async {
//Do something with the user
});
userSubscription!.onError((e) async {
await logout();
emit(
RootCubitError(state.user, e.toString()),
);
}
}
My test is structured so when _listenToUser is called, it throws. However, that is not what actually happens when the stream fails. Again, it instead calls onError.
My test:
test('should logout when user listener throws error', () async {
_auth = MockFirebaseAuth(
signedIn: true,
mockUser: MockUser(uid: parent1.id, email: parent1.email),
);
var cubit = _cubit(_auth);
when(_getLoggedInChildId()).thenReturn(null);
when(_listenToUser(any)).thenThrow(Exception()); //This is wrong, as it throws an error, instead of calling onError
cubit.init();
await expectLater(
cubit.stream,
emitsInOrder(
[
isA<RootCubitError>(),
],
),
);
verify(_sharedPreferences.clear());
verify(_auth.signOut());
});
and my stream:
#override
Stream<User> user(String uid) {
return FirebaseFirestore.instance
.collection(FirestoreCollections.users)
.doc(uid)
.snapshots()
.map((doc) => UserModel.fromMap(doc.id, doc.data())); //This map can fail and call onError
}
Any idea on how I can mock the call to onError in this case?

It is only possible to mock methods of mock classes. For example, in your case you could mock when(_auth.anyMethod(any)).thenThrow(Exception()) because _auth is a mock class.
It's not clear if cubit is a mock class. If it is you could change the method _listenToUser from private to public in your Cubit and add the #visibleForTestign annotation to warn you not to use it unless in a test like so:
#visibleForTesting
listenToUser(String uid) {
}
Then you can do when(cubit.listenToUser(any)).thenThrow(Exception())

Related

Flutter Riverpod Async Notifier | Unhandled Exception: Bad state: Future already completed

I am using the riverpod_generator, and the AsyncNotifier in riverpod
part 'auth_controller.g.dart';
// 3. annotate
#riverpod
// 4. extend like this
class AuthController extends _$AuthController {
// 5. override the [build] method to return a [FutureOr]
#override
FutureOr<void> build() {
// 6. return a value (or do nothing if the return type is void)
}
Future<void> signIn({
required String email,
required String password,
}) async {
final authRepository = ref.read(authRepositoryProvider);
state = const AsyncLoading();
state = await AsyncValue.guard(
() async => await authRepository.signIn(email, password));
}
Future<void> logout() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await FirebaseAuth.instance.signOut();
});
}
}
So when I am trying to logout the error will be occur it said
Future already completed
How to solve this?
I solve this problem, actually I listen to the changes of FirebaseAuth currentUser, then it will automatically redirect either homepage (if logged in) or login page (if logout), therefore the error will be occurred because I subscribe to the changes of FirebaseAuth

Flutter Bloc Error : emit was called after an event handler completed normally - between two functions

I have the following problem...
emit was called after an event handler completed normally. This is
usually due to an unawaited future in an event handler. Please make
sure to await all asynchronous operations with event handlers and use
emit.isDone after asynchronous operations before calling emit() to
ensure the event handler has not completed.
BAD on((event, emit) {
future.whenComplete(() => emit(...)); });
GOOD on((event, emit) async {
await future.whenComplete(() => emit(...)); }); )
What happens is that in a function called _onLogIn, if the user has changed the language, it goes from there to another function inside the bloc, these two functions do not depend on each other, I mean that each function is called in different pages of the application, but still _onLogIn checks the _onChangeLanguage function.
UserBloc({this.usecases}) : super(UserInitial()) {
on<LogInEvent>(_onLogIn);
on<ChangeLanguageEvent>(_onChangeLanguage);
}
_onLogIn function :
void _onLogIn(
LogInEvent event,
Emitter<StateA> emit,
) async {
emit(UserLoading());
final userOrFailure = await services.logIn(
x: event.x,
y: event.y,
);
await userOrFailure.fold((user) async {
/// If the user is logging in for the first time and does not
/// have a preferred language.
if (user.preferredLanguage == null) {
emit(UserSuccess());
emit(UserAlreadyLogged(connectedUser));
} else {
/// An ChangeLanguageEvent object
ChangeLanguageEvent event = ChangeLanguageEvent(
user.preferredLanguage,
user.someId,
);
/// Call the other function in the same bloc
this._onChangeLanguage(
event,
emit,
isFromLogin: true,
);
}
}, (failure) {
emit(UserError(failure.message));
});
}
_onChangeLanguage function :
void _onChangeLanguage(
ChangeLanguageEvent event,
Emitter<StateA> emit, {
bool isFromLogin = false,
}) async {
final successOrFailure = await services.updateLanguage(
event.language,
event.someId,
);
await successOrFailure.fold( // ! HERE THE ERROR WHEN I LOG IN; but when i changed the language from the application i don't have an error
(language) async {
emit(ChangeAppLanguage(language));
final sessionOrFailure = await services.getSession();
sessionOrFailure.fold(
(session) {
/// I need this condition to know if the language comes from login
if (isFromLogin) {
emit(UserSuccess());
}
emit(UserAlreadyLogged(session));
},
(failure) => emit(UserError(failure.message)),
);
},
(failure) {
emit(UserError(failure.message));
},
);
}
Any idea why? Thank you
void _onChangeLanguage(
ChangeLanguageEvent event,
Emitter<StateA> emit, {
bool isFromLogin = false,
}) async
This should be a major red flag. A call marked as async, but not returning a Future<>. There is no way, the caller could possibly await this call. Or even know that they should await this call.
Make it return a proper Future<void> instead of just void and your bloc should pick up on that and properly await the call.
There even is a linter rule for this: avoid_void_async. Did you turn off your linter? Don't do that. Turn your linter on and listen to it. Your other function has the same problem.
In my case I had to return Future<...>, but have not done it in the 'switch' statement, rather I've used the 'break' on all cases. So a compiler have not pointed me the lack of the 'return' statement.
After I have put 'return' on all cases, the error disappeared.

Migrate to BLoC 7.2- Nested Streams - yield* inside other stream

I'm migrating a project from Bloc 7.0 to 7.2
I have an issue trying handle the migration of this following Stream since it is calling another Stream within it self :
Stream<CustomerState> _mapUpdateNewsletter({...}) async* {
try {
[...]
yield* _mapGetCustomer(); // Calling another Stream here
Toast.showSuccess(message: successMessage);
} ...
}
Here is what the called Stream used to look like
Stream<CustomerState> _mapGetCustomer() async* {
try {
final customer = await _customerRepository.getCustomer();
yield state.getCustomerSuccess(customer);
} catch (error, stackTrace) {
ApiError.handleApiError(error, stackTrace);
}
}
Here is what I migrated it to :
Future<void> _onGetCustomer(
GetCustomer event, Emitter<CustomerState> emit) async {
try {
final customer = await _customerRepository.getCustomer();
emit(state.getCustomerSuccess(customer));
} catch (error, stackTrace) {
ApiError.handleApiError(error, stackTrace);
}
}
How am I suppose to call it now in Bloc 7.2 ?
Future<void> _onUpdateNewsletter(UpdateNewsletter event, Emitter<CustomerState> emit) async {
try {
...
yield* _onGetCustomer; // How do I call this async future here?
Toast.showSuccess(message: event.successMessage);
} ...
}
in the new version of the bloc, you don't have to write stream functions. you have a function called emit and calling this function and passing the new state is possible from every function in your bloc. so remove yield* and just call _onGetCustomer function and from there emit your new state.

Flutter Navigator pushnamed object not working when sent to class as parameter

I am trying to refactor my code and use a separate class object to register users and then send them to the correct page which is either the awaiting for email confirmation screen(if they haven't confirmed their email address) or the actual chat screen in the app.
I pass the Navigator.pushNamed object from the Flutter material build to the separate class object, but the Navigator.pushNamed doesn't work when it is sent as a parameter to the class object. It only worked when it is in the main build.
This is the main section that collects the parameters and sends them to the class
Widget build(BuildContext context) {...
Registration(
email: email,
password: password,
awaitingEmailConfPage:
() async {
await Navigator.pushNamed(context, EmailConfirmation.id);
},
confirmedEmailConfPage: () async {
await Navigator.pushNamed(context, ChatScreen.id);
}
).registerNewUser();
This is the class object that receives the Navigator.pushNamed but doesn't want to allow it to work and send users to the correct screen.
class Registration {
Registration(
{required this.awaitingEmailConfPage,
required this.confirmedEmailConfPage});
final _auth = FirebaseAuth.instance;
Future Function() awaitingEmailConfPage;
Future Function() confirmedEmailConfPage;
void registerNewUser() async {
try {
User? user = _auth.currentUser;
if (user!.emailVerified) {
await awaitingEmailConfPage;
} else {
await confirmedEmailConfPage;
}
}
}
If you want to call a function that receive as parameter you need to use call() method:
if (user!.emailVerified) {
await awaitingEmailConfPage.call();
} else {
await confirmedEmailConfPage.call();
}

How to test exceptions thrown from an async generator?

I have a repository class in my Flutter app with the following method that returns a Stream:
Stream<List<Product>> getProducts() async* {
var currentUser = await this._auth.currentUser();
if (currentUser == null) {
throw AuthException('not_logged_in',
'No current user found probably because user is not logged in.');
}
yield* ...
}
As per this answer on SO, the above way to throw an exception from an async generator function looks fine.
How do I write my test (with test package) so to test the exception thrown by this method?
Something like this does not work:
test('should throw exception when user is not logged in', () {
final _authSignedOut = MockFirebaseAuth(signedIn: false);
final _repoWihoutUser = FirebaseProductRepository(
storeInstance: _store,
authInstance: _authSignedOut,
);
var products = _repoWihoutUser.getProducts();
expect(products, emitsError(AuthException));
});
Nor this:
expect(callback, emitsError(throwsA(predicate((e) => e is AuthException))));
Not even this:
var callback = () {
_repoWihoutUser.getProducts();
};
expect(callback, emitsError(throwsA(predicate((e) => e is AuthException))));
You're close. Your first attempt:
expect(products, emitsError(AuthException));
doesn't work because emitsError takes a Matcher as its argument, so you can't pass it a type directly. Instead, you need to use the isA<T>() Matcher:
expect(products, emitsError(isA<AuthException>()));