How to test async functions that update PublishSubject or BehaviorSubject object (RxDart) in Flutter - flutter

I've been learning flutter for a few weeks and come from an Android background so far I love it and I have also been delighted to find that Flutter was designed with testing in mind from day one. However, I've been having an issue running the following test.
main() => {
test('test get popular repos', () async {
final testOwner = Owner(1010, "testLink");
final testRepo =
Repo(101, testOwner, "testRepo", "description", 'htmlUrl', 500);
final testRepoResponse = RepoResponse(List.from([testRepo]), null);
final uiModel = PopRepo(testRepo.owner.avatarUrl, testRepo.name,
testRepo.description, "Stars: ${testRepo.stargazersCount}");
final searchData = SearchData(List.from([uiModel]), null);
final Repository mockRepository = _mockRepository();
when(mockRepository.getPopularReposForOrg("org"))
.thenAnswer((_) => Future.value(testRepoResponse));
final repoSearchBloc = RepoSearchPageBloc(mockRepository);
await repoSearchBloc.getPopularRepos("org");
await expectLater(repoSearchBloc.resultSubject.stream, emits(searchData));
}),
};
class _mockRepository extends Mock implements Repository {}
My RepoSearchBloc takes data from a Repository and transforms it into the Ui model. Finally it posts that now UI-ready data to the Subject
this is the method under test in the RepoSearchBloc
getPopularRepos(String org) async {
if (org == null || org.isEmpty)
return resultSubject.add(SearchData(List(), null));
RepoResponse response = await _repository.getPopularReposForOrg(org);
if (response.error == null) {
List<Repo> repoList = response.results;
repoList.sort((a, b) => a.stargazersCount.compareTo(b.stargazersCount));
var uiRepoList = repoList
.map((repo) => PopRepo(repo.owner.avatarUrl, repo.name,
repo.description, "Stars: ${repo.stargazersCount}"))
.take(3)
.toList();
resultSubject.add(SearchData(uiRepoList, null));
} else {
ErrorState error = ErrorState(response.error);
resultSubject.add(SearchData(List(), error));
}
When I run the test I keep getting this message no matter what I do it seems with either BehaviorSubject or PublishSubject:
ERROR: Expected: should emit an event that <Instance of 'SearchData'>
Actual: <Instance of 'BehaviorSubject<SearchData>'>
Which: emitted * Instance of 'SearchData'
Any ideas how to get this test to pass?

Ended up figuring this out with the help of a user Nico #Rodsevich of the Flutter Glitter community
anyways using his suggestion to use await for
I came up with the following solution which passed
await for (var emittedResult in repoSearchBloc.resultSubject.stream) {
expect(emittedResult.results[0].repoName, testRepo.name);
return;
}
The RxDart library has some subject tests for reference but my subject being posted to asynchronously did not adhere to their test cases so this solution ended up being just what I needed.
Also #Abion47 's comment also seems to do the job when I move async inside the parameter for expected
expectLater( (await repoSearchBloc.resultSubject.stream.first as SearchData).results[0].repoName, testRepo.name);

Related

Flutter function returning at await statement

I am using flutter with the cbl package to persist data. Trying to retrieve the entries does not seem to work because the function created is returning at the await statement and not the return statement. This does not seem like the intended result of darts async/await functionality. So I am lost.
task_database.dart
Future<dynamic> getAllTasks() async {
final tasksDb = await Database.openAsync(database); <---------- Returns here
var tasksQuery = const QueryBuilder()
.select(SelectResult.all())
.from(DataSource.database(tasksDb));
final resultSet = await tasksQuery.execute();
late var task;
await for (final result in resultSet.asStream()) {
final map = result.toPlainMap();
final taskDao = TaskDao.fromJson(map);
task = taskDao.task;
// Do something with the task...
print(task);
}
;
return task; <-------------------------------------------- Does not make it here
}
task_cubit.dart
getAllTasks() => {
allTaskMap = TasksAbcDatabase().getAllTasks(),
emit(TaskState(tasks: state. Tasks))
};
What I have tried. I have tried to use Database.openSync instead of Database.openAsync however, the function just returns at the next await statement. I have also tried making getAllTasks asynchronous and awaiting the database as such.
Future<void> getAllTasks() async => {
allTaskMap = await TasksAbcDatabase().getAllTasks(),
emit(TaskState(tasks: state. Tasks))
};
However this has the same issue, when the function from task_database returns prematurely it the returns at the first await function in getAllTasks which is the allTaskMap variable.
Thanks
A function cannot "return prematurely" without a return statement.
The only way the execution is cut short would be an exception being thrown.
I also don't see how you don't get syntax errors, when you don't await the Database.openAsync(database) statement.
So make sure all your awaits are in place. Use the linter to find those that are missing. While you are at it, remove the keyword dynamic from your vocabulary, it will only hurt you if you use it without a need for it. Your return type should be properly typed, then your compiler could tell you, that returning a single task from a function that is clearly supposed to return multiple tasks is not going to work.
Either catch your exceptions and make sure you know there was one, or do not catch them and watch them go all the way through into your debugger.
In addition, following the comment of #jamesdlin, your function definitions are... valid, but probably not doing what you think they are doing.
Future<void> getAllTasks() async => {
allTaskMap = await TasksAbcDatabase().getAllTasks(),
emit(TaskState(tasks: state. Tasks))
};
needs to be
Future<void> getAllTasks() async {
allTaskMap = await TasksAbcDatabase().getAllTasks();
emit(TaskState(tasks: state. Tasks));
}

Flutter Async Function Update Doc and Use In Same Function

I have a function that does several things including update a document where the updated document is then used later on the in the function to update another set of documents. In this case the ChallengesRecord challenge is first updated by adding the current user to the challenge. Then, a second set of documents are updated by updating the list of users that are eventually sent a push notification through a collection called ff_user_push_notifications.
Everything seems to be working except the notifications record is not being updated with the updated challenges doc and pulling in data from the doc before it was updated. Here is what I have
Future<void> joinUserToChallenge(ChallengesRecord challenge) async {
//this updates the ChallengesRecord challenge
final challengesUpdateData = {
'users_inchallenge': FieldValue.arrayUnion([currentUserReference])
};
await challenge.reference.update(challengesUpdateData).then((value) =>
print('success'));
//this then queries the notification documents to be updated then
//iterates through them to update each one on a batch.commit()
//however it is not using the updated challenges doc it appears
final batch = FirebaseFirestore.instance.batch();
await FirebaseFirestore.instance
.collection('ff_user_push_notifications')
.where(filter 1)
.where(filter 2)
.get().then((snapshots) => {
snapshots.docs.forEach((doc) => {
batch.update(doc.reference, {
"user_refs" : challenge
.usersInchallenge.map((u) => u.path).join(',')
}),
print(snapshots.docs.length)
})
});
await batch.commit();
}
Console output below which suggests the challenge doc is being updated first.
flutter: success
flutter: 2
So it appears the batch.update() is not waiting on/using the updated challenge doc. Any suggestions?
UPDATE: I also tried the code below, but it is still not working.
Future<void> joinUserToChallenge(ChallengesRecord challenge) async {
//this updates the ChallengesRecord challenge
final challengesUpdateData = {
'users_inchallenge':
FieldValue.arrayUnion([currentUserReference])
};
await challenge.reference.update(challengesUpdateData).then((value)
=> print('success'));
final batch = FirebaseFirestore.instance.batch();
await FirebaseFirestore.instance
.collection('ff_user_push_notifications')
.where(filter 1)
.where(filter 2)
.get();
pushNotificiations.docs.forEach((doc) => {
batch.update(doc.reference, {
"user_refs" : challenge
.usersInchallenge.map((u) => u.path).join(',')
}),
});
await batch.commit()
}

Riverpod: Is there a correct way to read a StreamProvider within another StreamProvider?

I have been attempting to create streams to Firestore documents using the uid obtained from my auth Provider:
class AuthService {
...
static final provider = StreamProvider.autoDispose((ref) => FirebaseAuth.instance.onAuthStateChanged);
...
}
However, I am struggling to actually create a StreamProvider dependent on the value from the auth Provider.
class User {
...
static final provider = StreamProvider((ref) {
final stream = ref.read(AuthService.provider);
// Returns AsyncValue<Stream<User>> instead of desired AsyncValue<User>
return stream.map((auth) => Service.user.stream(auth.uid));
});
...
}
I also tried using Computed to return the uid or the stream itself but you cannot read a Computed from a Provider (which makes sense in retrospect).
This question is the most relevant on this topic but it is dealing with Provider, not Riverpod.
P.S. Can a Riverpod tag be created?
Edit:
The answer isn't working quite right. The await for loop is only ever triggering once, whereas a listener catches all events.
static final provider = StreamProvider((ref) async* {
final stream = ref.read(AuthService.provider);
print('userProvider init');
stream.listen((auth) {
print('LISTENED: ${auth?.uid}');
});
await for (final auth in stream) {
print('uid: ${auth?.uid}');
yield* Service.user.stream(auth?.uid);
}
});
This code yields the following on login:
userProvider init
LISTENED: <redacted UID>
uid: <redacted UID>
And then on logout:
LISTENED: null
Where I would expect to see uid: null as well, which would update the stream, but upon any more auth events, only the listener is triggered and no events are caught by the await for loop.
Interestingly, using the flutter inspector, the value emitted by the auth provider never changes, either:
AutoDisposeStreamProvider<FirebaseUser>#95f11: AsyncValue<FirebaseUser>.data(value: FirebaseUser(Instance of 'PlatformUser'))
persists through login/logout events, which could explain this behavior, but I am not sure what to do to fix it.
Any ideas? I have been stuck on this for a while and can't correct the issue.
The problem is, your provider doesn't create a Stream<User> but a Stream<Stream<User>>
As part of 0.6.0-dev, you can use ref.watch to easily combine streams:
class User {
...
static final provider = StreamProvider((ref) {
final auth = ref.watch(AuthService.provider);
return Service.user.stream(auth.uid);
});
...
}
I want to preface this by saying I've only been working with Dart/Flutter for a few months, so feel free to correct anything I say and I will update the answer.
I solved this issue after much trial and error and re-reviewing documentation many times. I guess I made a poor assumption that Providers would update when a Provider they depend on changes.
I found that once a StreamProvider returns (or yields), until it is disposed it will always return the same value it originally did, regardless of dependency changes or events coming from a Stream. This is where Computed is useful but doesn't really work well when you desire to return AsyncValue from your provider (how StreamProvider behaves).
Also, additional confusion was caused by the Flutter Inspector not updating the ProviderScope widget correctly. I have to click around and refresh a few times to see updates to Providers or their state. Anyways...
class AuthService {
...
static final provider = StreamProvider.autoDispose((ref) {
final sub = FirebaseAuth.instance.onAuthStateChanged.listen((auth) => ref.read(uidProvider).state = auth?.uid);
ref.onDispose(() => sub.cancel());
return FirebaseAuth.instance.onAuthStateChanged;
});
static final uidProvider = StateProvider<String>((_) => null);
...
}
class User {
...
static final provider = StreamProvider.autoDispose((ref) {
final uid = ref.read(AuthService.uidProvider)?.state;
return Service.user.stream(uid);
});
...
This solution works given that your UI no longer depends on providers (allowing them to be properly disposed) when a user signs out of your app.

Trouble with testing using MockClient in Flutter

I am trying to write a simple test in flutter using MockClient, but I can't seem to get it to work.
Here is the code I am trying to test:
getItemById(int id) async {
final response = await client.get("$_host/item/$id.json");
final decodedJson = json.decode(response.body);
return Item.fromJson(decodedJson);
}
Here is the test code:
test("Test getting item by id", () async {
final newsApi = NewsAPI();
newsApi.client = MockClient((request) async {
final jsonMap = {'id': 123};
Response(json.encode(jsonMap), 200);
});
final item = await newsApi.getItemById(123);
print("Items: ${item.toString()}"); //<-- dosen't print anything.
expect(item.id , 123);
});
When I run the test, it fails with the following message:
NoSuchMethodError: The getter 'bodyBytes' was called on null.
Receiver: null
Tried calling: bodyBytes
I am guessing the issue here is that nothing is returned from the MockClient when I make the call to the getItemById method, but I am not sure why.
I had the same exact issue. You have to return the Response
return Response(json.encode(jsonMap), 200);
Mock expects test function to be EXACTLY as you real function (including OPTIONAL parameters and so on). If both does not match it returns NULL and that is what is happening with your code. Double check to see where your test function is different of original function.

How to filter data from backend using bloc future fetch stream?

I have this method on bloc
fetchProductAttribute(int _prodId) async {
List<ProductAttribute> productAttribute = await _repository.fetchProductAttribute(_prodId).catchError((err) => _fetcher.addError(err));
_fetcher.sink.add(productAttribute);
}
This will call repository then rest api to the backend. Now I want to filter this data.
I will modify this but it's not working... How to do this properly?
fetchProductAttribute(int _prodId) async {
List<ProductAttribute> productAttribute = await _repository.fetchProductAttribute(_prodId).catchError((err) => _fetcher.addError(err));
for(var c in productAttribute){
if(c.variant==true){
_fetcher.sink.add(productAttribute);
}
}
}
All data still coming up...I want only data with variant true to be displayed on screen. How to do this?
I seen one article on how to do filtering on Stream by using StreamSubscription here and here but this is not what I want. I want to filter out from the earlier part of REST.
As one comment said, use the where operator over the list.
fetchProductAttribute(int _prodId) async {
List<ProductAttribute> productAttribute = await _repository.fetchProductAttribute(_prodId).catchError((err) => _fetcher.addError(err));
productAttribute = productAttribute.where((c) => c.variant == true).toList()
_fetcher.sink.add(productAttribute);
}