How to unit test Flutter's StreamSubscription onData? - flutter

This is what I tried so far.
Let's say result is a StreamSubscription.
This is my flutter file
try {
result.listen(
(event) async {
// This converts the JSON data
final news = NewsModel.fromJSON(jsonDecode(event));
// This saves the data to local database
await localDataSource.saveNews([news]);
},
onError: (e) {
debugPrint('$e');
},
);
} catch (e) {
debugPrint('$e');
}
this is my flutter test since I want to test if method localDataSource.saveNews() fails
await newsRepository.subscribe(); calls the try catch above
controller is a StreamController to add new data to the stream
news is a dummy data, it doesn't matter because whatever the localDataSource do it will throw a LocalDBException
also I am using Mockito https://pub.dev/packages/mockito to mock the localDataSource above
test(
'should handle fail save news method',
() async {
// arrange
when(mockLocalDataSource.saveNews(any)).thenThrow(LocalDBException());
// act
await newsRepository.subscribe();
controller.add(news)
// assert
},
);
As you can see I don't have any condition to pass the flutter test, but that's beyond the point as this flutter test already breaks the stream even if I have a onError on my listener.
if I use controller.addError(LocalDBException()) the onError works, but if I deliberately throw an exception from the method localDataSource.saveNews() it breaks the stream.
Given this context I want to know 2 things:
I want to know how to handle the error inside the onData of StreamSubscription, if a method / function throw an exception, as it ignores the onError if a method / function throw an exception inside the listener.
Is adding an error through addError() function the same as throwing an exception inside the stream?

Related

Issues handling Navigation when exception occurs [Bloc]

I'm creating a flutter app using bloc/cubit pattern. I'm having issues preventing navigation when an exception occurs, here's my button onTap function:
function: () async {
if (state.termsAndConditionsAccepted &&
state.status.isValid &&
!state.status.isPure) {
await context
.read<SignUpCubit>()
.onClickSignUp()
.then(((value) => {Navigator.pop(context)}));
}
and here's the logic of sign in
Future<void> onClickSignUp() async {
try {
// sign in logic...
// Provoking an exception...
throw Exception('An error ocrred. Please try again.');
} on Exception catch (exception) {
String exMessage = exception.toString();
log(exMessage);
// This emits a new state with the error and shows an error snackbar based on this new state
emit(state.copyWith(
status: FormzStatus.submissionFailure,
exceptionErrorMessage: exMessage));
}
As far as I know, the .then() callback function will only be executed if the Future function completes successfully(onClickSignUp()) but the Navigation occurs even if the exception is thrown.
I managed make it work by returning a boolean value to the then() callback and do the navigation depending in that value(if true navigate/if false do nothing), but that would require me to return true when function completes successfully and return false in all my catch blocks which does not seems a good practice.
Since I'm handling the states from my cubit class I cannot use .onError or .catchError() callbacks on the button's function.
Hopefully someone can suggest a better error handling in this case or what could be an appropriate error handling for bloc pattern

Mocking Hive with Mockito Flutter

So basically, I want to check whether I've passed whatever I need to pass to the HiveInterface and Box when I want to store something.
test.dart:
group('cacheStoraygeUser', () {
test(
'should call HiveInterface and Box to cache data',
() async {
when(mockHiveInterface.openBox(any)).thenAnswer((_) async => mockBox);
when(mockBox.put(0, tStoraygeUserModel))
.thenAnswer((_) async => tStoraygeUserModel);
// act
dataSourceImpl.cacheStoraygeUser(tStoraygeUserModel);
// assert
verify(mockHiveInterface.openBox(STORAYGE_USER_BOX));
verify(mockBox.put(STORAYGE_USER_ENTRY, tStoraygeUserModel));
},
);
});
My implementation for dataSourceImpl.cacheStoraygeUser():
#override
Future<void> cacheStoraygeUser(
StoraygeUserModel storaygeUserModelToCache) async {
/// Precaution to ensure that [STORAYGE_USER_BOX] has been opened.
///
/// If the box, is in fact not opened, Hive will just return the box since
/// the box is a Singleton. I think.
final box = await hiveInterface.openBox(STORAYGE_USER_BOX);
box.put(STORAYGE_USER_ENTRY, storaygeUserModelToCache);
}
When I try to run the test, it gives this error:
type 'Null' is not a subtype of type 'Future<void>'
MockBox.put
package:hive/…/box/box_base.dart:80
I already generated the mock classes for HiveInterface and Box. I think this is how I should do it if I want to test Hive, since I can't seem to generate Mock classes for Hive itself. But if you know a better or the correct solution then please tell me.
I also wrote another test for getting stuff from Hive. This works perfectly fine.
test(
'should return StoraygeUser from StoraygeUserBox when there is one in the cache',
() async {
// arrange
when(mockHiveInterface.openBox(any)).thenAnswer((_) async => mockBox);
when(mockBox.getAt(any)).thenAnswer((_) async => tStoraygeUserModel);
// act
final result = await dataSourceImpl.getCachedStoraygeUser();
// assert
verify(mockHiveInterface.openBox(any));
verify(mockBox.getAt(any));
expect(result, equals(tStoraygeUserModel));
},
);
Thanks in advance!
This problem has been fixed in Mockito 5.0.9
The problem stems from the fact that Box implements BoxBase rather than extends it.
Older versions of Mockito couldn't pick this up and thus, putAt and getAt and other methods are not generated in the mock classes.

Flutter firebase onBackgroundMessage throws exception

When i add this function to my code.
FirebaseMessaging.onBackgroundMessage(
(message) => _handleBG(message, currentUserId));
I get exception
[ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: NoSuchMethodError: The method 'toRawHandle' was called on null.
onMessage method below is working fine.
FirebaseMessaging.onMessage.listen((event) async {
// event.messageType
print('hearing to foreground messsage');
if (event.data['messageType'] == 'chat') {
await ChatHelper.handleMessageReceived(event, context, currentUserId);
//setState(() {});
}
});
FirebaseMessaging.onBackgroundMessage(
(message) => _handleBG(message, currentUserId));
The background message handler should be declared as a top-level function so that it can be called independently, as you never know the state of your app and its object when the notification is received. It looks like that your _handleBG method is a private method available to a specific class only.
From code documentation
/// This provided handler must be a top-level function and cannot be
/// anonymous otherwise an [ArgumentError] will be thrown.

How to catch specified exception, display warning and logout effectively?

Web application contains many screens with list views and edit dialogs for edit selected records. List view uses "standard display behavour" using StreamBuilder.
Simplified bloc.dart
class Bloc {
final _subject = StreamController<Data>.broadcast();
Stream<Data> get data => _subject.asyncMap(_getData);
Future<Data> _getData(...) {
try {
final data = await api.getUrl(...);
return data;
} on SomeException catch (e) {
// This exception is catched by StreamBuilder
throw ApiException('could not get data');
}
}
}
Simplified widget.dart
StreamBuilder<...>(
stream: bloc.data,
builder: (context, snapshot) {
if (snapshot.hasError) {
// Bloc throws an exception which thrown by API client (HttpClient).
return ErrorWidget(snapshot.error.toString());
}
if (snapshot.hasData) {
return ListView.builder(...);
} else {
return CircularProgressIndicator();
}
}
),
Each edit dialog uses showDialog to display selected item details and save modifications.
There may be a situation when (for example) access token is invalid (or expired) during his already authenticated session. It is necessary to inform user about this (showing him a warning) and after closing warning destroy the session and return to login screen. Is it possible to implement effective solution to catch specific exception (eg. TokenInvalidException) in a single place without adding symbolic code like:
if (snapshot.error is TokenInvalidException) {
// destroy session
// return to Login
}
into each StreamBuilder list view and some code to check if exception occurred on each Dialog's save action?
I have 100500 screens and I do not want to implement the same code for each screen. Actually I want some global exception catcher which catches the required exception and executes specific action.
Now I see some global StreamController with stream which accepts such exception (events) and is listen by some global parent widget (MaterialApp or maybe Main).
final globalExceptionController = StreamController<TokenInvalidException>.broadcast();
// somewhere
globalExceptionController.stream.listen((e) async {
// Show warning
final result = await showDialog(...);
// Go to login
Navigator.pushReplacement(context, <login page route>);
}) {}
The problem I see here is the bloc object which throws the exception will have strong dependency on stream controller which is not interested to it absolutely. But actually I need to link somehow the bloc and GUI because to show warning and switch the route I need a context.
Any ideas or critics are appreciated.
Maybe the coordinator bloc pattern described here helps you with this problem

Flutter, testing future that throws an Error makes the test unloadable

I'm trying to run a flutter test where a widget displays an error page when the Future provided to it throws an error (via FutureBuilder).
However the line where I create the future seems to be making the test fail.
final futureError = Future.delayed(Duration(milliseconds: 20))
.then((value) => throw Error());
with the message
Failed to load "D:\Projects\flutter\....dart": Instance of 'Error'
Putting it inside the test function resolved the issue
testWidgets('...',
(WidgetTester tester) async {
await tester.runAsync(() async {
final futureError = Future.error('error');
// ...
(it was in the group method prior to this)