How to test exceptions thrown from an async generator? - flutter

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>()));

Related

async/await method throws exception -type int is not a subtype of bool

I am calling a graphql endpoint which returns success, but I do get an exception on the calling method.
Here is my calling method -
await AmplifyInstance()// this is where I get the exception. Snip below
.createUserOnAzureCosmosDB(user)
.then((result) {
print(result['data']['userPhoneNumber']);
_intlPhoneFieldController.text =
(result['data']['userPhoneNumber'].toString())
.substring(1);
_incrementStep('continueOnProfilePictureWidget');
});
Here is the called method -
Future<dynamic> createUserOnAzureCosmosDB(User user) async {
HttpLink link = GlobalVariables().graphqlEndpoint;
GraphQLClient graphQlClient = GraphQLClient(
link: link,
cache: GraphQLCache(
store: InMemoryStore(),
),
);
try {
QueryResult mutationResult = await graphQlClient.mutate(
//Mutation query here
if (mutationResult.data?['createUser'] != null) {
print('Created user on Cosmos DB');
registerUserStatus['result'] = true;
registerUserStatus['data'] = mutationResult.data?['createUser'];
}
} on ApiException catch (e) {
print('Mutation failed: $e');
registerUserStatus['result'] = false;
registerUserStatus['errorMessage'] = e.message;
}
return registerUserStatus;
}
And the returned registerUserStatus is just an array -
var registerUserStatus = {};
Here is the exception -
UPDATE eamirho3ein
Here is the result of print("result=$result);
I/flutter (14224): result = {result: true, data: {__typename: User, partitionKey: user, userPhoneNumber: 14160000000, userDisplayName: testuser, avatarUrl: www.url.com, createdAt: Today}}
This is not actually an answer, but rather a way to find the answer more easily yourself:
then chains make it increasingly hard to find your problem, because the compiler/debugger/IDE has a harder time pointing you to it. So don't do it.
With async/await available from the beginning, there never has been a reason to use then in any Dart program.
await AmplifyInstance().createUserOnAzureCosmosDB(user).then((result) {
Is equivalent to just writing:
final result = await AmplifyInstance().createUserOnAzureCosmosDB(user);
And then continuing on with the code you had put in the lambda function in the then part. Obviously, you need to remove the closing bracket somewhere too now.
This way, your error will actually pop up where it happens, not at the await of a huge chain that leaves you wondering what the problem might be.

Not able to print debug message while unit testing in Flutter

I am testing a pretty straightforward use-case in Flutter. Inside the use-case class, I have a function that I'm invoking from my test. And I want to add some debug print statements to print the value of some variables inside the function of use-case. But it's not getting printed anywhere. How can I achieve this?
The function in Use-case.
Future<Either<Failure, List<Contest>>> call(NoParams params) async {
final result = await repository.getAllContests();
final currentDateTime = DateTime.now();
List<Contest> ongoingContests = [];
result.fold(
(l) => throw ServerException(),
(allContestList) => () {
for (var contest in allContestList) {
var contestStartTime = DateTime.parse(contest.start_time);
var contestEndTime = DateTime.parse(contest.end_time);
print(contestEndTime); //print statement
}
});
return Right(ongoingContests);
}
}
The test function
test('Should return only the ongoing contests', () async {
when(mockHomepageRepository.getAllContests()).thenAnswer((_) async =>
const Right([tContest, tOngoingContest, tUpcomingContest]));
final result = await getOngoingContests(NoParams()); //invoking the function
expect(result, const Right([tOngoingContest]));
verify(mockHomepageRepository.getAllContests());
verifyNoMoreInteractions(MockHomepageRepository());
});

Riverpod giving a bad state exception when one hits back button on webpage

I'm getting this error in my StateNotifiers when one hits the back button on their webpage. I've isolated it to happening where the longRunningAPI request is below.
Exception has occurred.
"Error: Bad state: Tried to use RunListNotifier after `dispose` was called.
and I have code like this.
final runListController = StateNotifierProvider.autoDispose
.family<RunListNotifier, AsyncValue<List<Run>>, RunListParameter>(
(ref, param) {
return RunListNotifier(read: ref.read, param: param);
});
class RunListNotifier extends StateNotifier<AsyncValue<List<Run>>> {
RunListNotifier({required this.read, required this.param})
: super(AsyncLoading()) {
fetchViaAPI(param);
}
final Reader read;
final RunListParameter param;
void fetchViaAPI(RunListParameter param) async {
state = AsyncLoading();
try {
List<Run> stuff = await read(apiProvider).longRunningAPI(param: param);
state = AsyncData(stuff);
} catch (e) {
state = AsyncError(e);
}
}
}
is it safe to simply do something like this in the catch?
} catch (e) {
if (e.runtimeType.toString() == 'StateError') {
// ignore the error
} else {
state = AsyncError(e);
}
}
I believe you could solve this problem by checking mounted before setting the state after your API call like so:
List<Run> stuff = await read(apiProvider).longRunningAPI(param: param);
if (!mounted) return;
state = AsyncData(stuff);
This simply checks if dispose was called and if so, don't attempt to modify the state.
Another resource that could be useful is adding a cancelToken to your API call and canceling if the provider is disposed.
final longRunningApi = FutureProvider.autoDispose.family<List<Run>, RunListParameter>((ref, param) async {
final cancelToken = CancelToken();
ref.onDispose(cancelToken.cancel);
final api = await ref.watch(apiProvider);
final res = await api.longRunningApi(param, cancelToken);
ref.maintainState = true;
return res;
});
Then you'd have to add the cancelToken to your actual request. A great example of this in the marvel example project by the author of Riverpod can be found here.

Why the function is returning an empty list in Flutter

So Here's the code. The error is when calling loadSounds function from outside the class returns an empty list. But when loadSounds is called from loadcategories it works fine and returns a list containing instances of Sound. Even priniting the sounds variable in the loadSounds function prints an empty list.
class AudioRepository {
List<Category> categories = <Category>[];
List<Sound> sounds = <Sound>[];
Future<String> _loadCategoriesAsset() async =>
await rootBundle.loadString(Assets.soundsJson);
Future<List<Category>> loadCategories() async {
if (categories.isNotEmpty) {
return categories;
}
String jsonString = await _loadCategoriesAsset();
categories.clear();
categories.addAll(categoryFromJson(jsonString));
categories.map((c) => sounds.addAll(c.sounds)).toList();
loadSounds('1');
return categories;
}
Future<List<Sound>> loadSounds(String categoryId) async {
print(sounds);
return sounds
.where((sound) => sound.id.substring(0, 1) == categoryId)
.toList();
}
}
Output when called from loadCategories is as follows:
[Instance of 'Sound', Instance of 'Sound', Instance of 'Sound', Instance of 'Sound', Instance of 'Sound', Instance of 'Sound']
I'm accessing it outside the class as follows:
final _sounds = await repository.loadSounds(event.categoryId);
Output when called from outside or printing from loadSounds function is as follows:
[]
So what's the problem here. I'm not able to figure out why the loadSounds fuction work when called from loadCategories inside the class and not otherwise in any way.
If you don't call repository.loadCategories() before calling loadSounds(), you won't have anything in your sounds variable since you are assigning values only in your loadCateogries() function.
Is your repository variable a singleton and did you call loadCategories on it?
Also, I would not write this line like this :
categories.map((c) => sounds.addAll(c.sounds)).toList();
The toList() method is not really usefull and the map function should be used more to convert something (a String to Int mapping for instance).
I would use :
categories.forEach((c)=> sounds.addAll(c.sounds));
use provider and ChangeNotifier is the best way.
your code will be like this
class AudioRepository extends ChangeNotifier {
List<Category> categories = <Category>[];
List<Sound> sounds = <Sound>[];
Future<String> _loadCategoriesAsset() async =>
await rootBundle.loadString(Assets.soundsJson);
Future<List<Category>> loadCategories() async {
if (categories.isNotEmpty) {
return categories;
}
String jsonString = await _loadCategoriesAsset();
categories.clear();
categories.addAll(categoryFromJson(jsonString));
categories.map((c) => sounds.addAll(c.sounds)).toList();
//notify listeners
notifyListeners();
////
loadSounds('1');
return categories;
}
Future<List<Sound>> loadSounds(String categoryId) async {
print(sounds);
return sounds
.where((sound) => sound.id.substring(0, 1) == categoryId)
.toList();
}
}
to retrieve data from outside, the following code should be placed in Build() methode.
final _arp = Provider.of<AudioRepository>(context, listen: true);
print(_arp._sounds);
It may be an error in the where clause. Because the condition isn't correct.
try this:
return sounds
.where((sound) => sound.id[0] == categoryId)
.toList();

Flutter test future throws an error, can't test exception

I'm looking to test a function that returns a Future
however when you pass in bad parameters it throws an exception.
I'm trying to write tests for this, however when I run
test("When a new practice is created with an empty name, an exception is thrown", () async {
//arrange
//act
//assert
expect(newUserRepository.saveUser("", "Test"), throwsA(TypeMatcher<Exception>()));
});
I get the following
Expected: throws <Instance of 'TypeMatcher<Exception>'>
Actual: <Instance of 'Future<void>'>
Which: threw ?:<Exception>
Any way I can get it to pass this test?
My saveUser function
Future<void> saveUser(String username, String practiceName) async {
if (username == null || username.isEmpty || practiceName == null || practiceName.isEmpty) {
throw Exception("Unable to save empty username or pracice name");
}
var firebaseUser = await userRepository.getFirebaseUser();
var newUser = DomainUser(userName: username, email: firebaseUser.email);
await userRepository.saveUser(newUser);
var newPractice = DomainPractice(users: [newUser.email], practiceName: practiceName);
await practiceRepository.savePractice(newPractice);
return;
}