I did stuck with testing streams that do some transformations before returning the value. I am using Firestore as my data storage and have a separated data layer that performs some mapping from DocumentSnapshot<T> to my model. Here is an example of the repo:
class RequestsRepository {
final CollectionReference<Request> _requestsCollection =
getIt<FirebaseFirestore>().collection('requests').withConverter<Request>(
fromFirestore: (snapshot, _) => Request.fromJson(snapshot.data()!),
toFirestore: (request, _) => request.toJson());
Stream<Request?> observe(String id) {
return _requestsCollection
.doc(id)
.snapshots()
.map((snapshot) => snapshot.exists ? snapshot.data() : null);
});
}
}
And now I'd like to cover observe(id) function with tests. Here is the solution I came to and below I'll explain why:
test('request exists', () async {
// WHEN
final stream = repository.observe(request.id);
// expected events
final events = [null, request, request..completed = true, null];
var eventIndex = 0;
// THEN
stream.listen(expectAsync1((value) async {
await Future.delayed(Duration(milliseconds: 100));
expect(value, events[eventIndex]);
eventIndex++;
}, max: -1));
// GIVEN
await firestore
.collection('requests')
.doc(request.id)
.set(request.toJson());
request.completed = true;
await firestore
.collection('requests')
.doc(request.id)
.set(request.toJson());
// reset request data
request.completed = false;
});
I tried emitsInOrder stream matcher however it fails, because I'm doing mapping inside the observe(id) function and that result in actually not request object but _MapStream<DocumentSnapshot<Request?>, Request?> instead because of the logic that stream map function follows:
Stream<S> map<S>(S convert(T event)) {
return new _MapStream<T, S>(this, convert);
}
The solution I came to did work for this case but it did not for other tests and also the solution is smelly. Any suggestions how to tests these kind of streams?
UPDATE:
Per comment from #pskink here is the demo test that fails with the same reason:
class Test {
Stream<B> observe() {
return Stream.periodic(Duration(milliseconds: 500), (i) => A(i * 10))
.map((i) => B('s${i.a}'));
}
}
class A {
final int a;
A(this.a);
bool operator ==(o) => o is A && a == o.a;
int get hashCode => a.hashCode;
}
class B {
final String b;
B(this.b);
bool operator ==(o) => o is B && b == o.b;
int get hashCode => b.hashCode;
}
void main() {
test('mapped stream: A > B', () async {
final stream = Test().observe();
expect(stream, emitsInOrder([B('s0'), B('s10'), B('s20'), B('s30')]));
});
}
It passes with the test so far.
Thanks to #pskink so far to identifying that the issue is not with stream conversion (even though logs look pretty unreadable). I had to make my Request object Equatable so it can now compare all fields appropriately. And I had to remove the await from firestore calls so that events occur really asynchronously.
Here is an example of working test:
test('request exists', () async {
// GIVEN
final timestamp = DateTime.now();
final request = Request(
'49a257e1-c0e3-4cc1-9053-aaf55197f897',
user.id,
"${user.firstName} ${user.lastName}",
user.phone ?? "",
'center',
'comment',
timestamp,
false,
false);
await firestore
.collection('requests')
.doc(request.id)
.set(request.toJson());
// WHEN
final stream = repository.observe('id', request.id);
// THEN
expect(
stream,
emitsInOrder([
Request(
'49a257e1-c0e3-4cc1-9053-aaf55197f897',
user.id,
"${user.firstName} ${user.lastName}",
user.phone ?? "",
'center',
'comment',
timestamp,
false,
false),
Request(
'49a257e1-c0e3-4cc1-9053-aaf55197f897',
user.id,
"${user.firstName} ${user.lastName}",
user.phone ?? "",
'center',
'comment',
timestamp,
false,
true),
]));
firestore
.collection('requests')
.doc(request.id)
.set({"completed": true}, SetOptions(merge: true));
});
Related
I'm quite new to unit testing and I want to test a method of a service that calls conditionally a second method. The problem I'm facing now is that I'm getting a missing MissingStubError even I have created the missing stub.
mockito: 5.3.2
Error Message:
MissingStubError: 'createCashbookItem'
No stub was found which matches the arguments of this method call:
createCashbookItem(Instance of 'CashbookItem')
Test:
#GenerateMocks([CashbookRepository, ReceiptRepository, CashbookItemRepository])
void main() {
late BookingService bookingService;
late MockCashbookRepository mockCashbookRepository;
late MockCashbookItemRepository mockCashbookItemRepository;
late MockReceiptRepository mockReceiptRepository;
setUp(() {
mockCashbookRepository = MockCashbookRepository();
mockCashbookItemRepository = MockCashbookItemRepository();
mockReceiptRepository = MockReceiptRepository();
bookingService = BookingServiceImpl(
receiptRepository: mockReceiptRepository,
cashbookItemRepository: mockCashbookItemRepository,
cashbookRepository: mockCashbookRepository,
);
});
group("bookings", () {
var t_OldReceipt = Receipt.empty();
var t_NewReceipt = Receipt.empty();
test("create booking", () async {
t_OldReceipt = t_OldReceipt.copyWith(
amount: 10,
totalAmount: 10,
taking: true,
category: "test",
taxRate: 0,
paymentType: kPaymentTypeBar,
documentNumber: "1",
taxAmount: 0,
receiptDate: DateTime.now(),
year: "2023",
yearMonth: "20232",
yearMonthDay: "2023210",
contact: Contact.empty());
// arrange
var cashbookItem = CashbookItem.fromReceipt(t_OldReceipt);
when(mockReceiptRepository.createReceipt(t_OldReceipt)).thenAnswer((realInvocation) async => const Right(unit));
when(mockCashbookItemRepository.createCashbookItem(cashbookItem)).thenAnswer((realInvocation) async => const Right(unit));
// act
final result = await bookingService.createBooking(t_OldReceipt);
// assert
expect(result, const Right(unit));
});
});
}
Method that I want to test:
Future<Either<ReceiptFailure, Unit>> createBooking(Receipt receipt) async {
Either<ReceiptFailure, Unit>? receiptFailureOrSuccess;
Either<cashbook_failures.CashbookFailure, Unit>? cashbookFailureOrSuccess;
receiptFailureOrSuccess = await receiptRepository.createReceipt(receipt);
if (receiptFailureOrSuccess.isLeft()) {
return receiptFailureOrSuccess;
}
if (receipt.paymentType == kPaymentTypeBar) {
cashbookFailureOrSuccess = await cashbookItemRepository.createCashbookItem(CashbookItem.fromReceipt(receipt));
if(cashbookFailureOrSuccess.isLeft()){
receiptRepository.deleteReceipt(receipt.id.value);
return left(CreateCashbookItemFromReceiptFailure());
} else {
return right(unit);
}
}
return receiptFailureOrSuccess;
}
I have added a stub for the error and my assumption was that it is possible to have two stubs in one test. I want to test if the method createCashbookItem inside createBooking is called.
So strange behavior for me, I am so confused, I have been with these 3 hours and still don't get it.
Problem :
I am calling an instance from a provider withe name “idea”
and later using that instance attribute in my widget.
This is my instance :
Widget build(BuildContext context) {
var idea = Provider.of<Idea>(context, listen: false).allideas.firstWhere(
(element) => element.id == widget.idea_id,
);
then I am calling another provider to call two functions
HTTP request
final connectionData =
await Provider.of<IdeaConnection>(context, listen: false)
.createIdeaConnection(
idea.userId,
idea.id,
);
final ideaProvider =
Provider.of<IdeaConnection>(context, listen: false);
await ideaProvider.uppdeaIdeaConnectionsprov(
idea.connections,
connectionData,
);
Future<dynamic> createIdeaConnection(int resiver_user_id, int idea_id) async {
final url = Uri.parse('http://10.0.2.2:3000/ideaconnetion');
var jwt = await storage.read(key: "token");
var userid = await storage.read(key: "id");
var response;
if (jwt != null) {
final Map<String, String> tokenData = {
"Content-Type": "application/json",
"token": jwt
};
try {
response = await http.post(url,
headers: tokenData,
body: json.encode({
"resiver_user_id": resiver_user_id,
"idea_id": idea_id,
}));
response = jsonDecode(response.body);
} catch (error) {
print(error);
rethrow;
}
}
return response;
}
calling another function to update data locally
Future<void> uppdeaIdeaConnectionsprov(
Map<int, IdeaConnectionsModel> connections, data) async {
var id = await storage.read(key: "id") as String;
var userData = await getLogedinUserData();
connections.putIfAbsent(
data['user_id'],
() => IdeaConnectionsModel(
id: data['id'],
uppdatedAtt: DateTime.parse(data['updatedAt']),
fName: userData['fName'] as String,
imge: stringToByteList(userData['image']!),
lNmae: userData['lName'] as String,
userId: int.parse(id),
userName: userData['userName'] as String,
accepted: false,
idea_Id: data['idea_id'],
));
}
What is strange is that the uppdeaIdeaConnectionsprov() do not return any map, but the map I am sending to the function gets updated and i see changes in my Text widget how is this possible......
> Text(
> "${idea.connections.length} Connections",
> style: const TextStyle(
> color: Colors.white,
> fontWeight: FontWeight.bold,
> fontSize: 12),
> ),
As stated in the documentation, putIfAbsent() modifies the current map. If you want the current map to remain unchanged and get a new map with new values, you can clone the existing map and call putIfAbsent() on the new map.
Future<Map<int, IdeaConnectionsModel>> uppdeaIdeaConnectionsprov(
Map<int, IdeaConnectionsModel> connections, data) async {
var id = await storage.read(key: "id") as String;
var userData = await getLogedinUserData();
return Map<int, IdeaConnectionsModel>.from(connections)..putIfAbsent(
data['user_id'],
() => IdeaConnectionsModel(
id: data['id'],
uppdatedAtt: DateTime.parse(data['updatedAt']),
fName: userData['fName'] as String,
imge: stringToByteList(userData['image']!),
lNmae: userData['lName'] as String,
userId: int.parse(id),
userName: userData['userName'] as String,
accepted: false,
idea_Id: data['idea_id'],
));
}
For a stream matcher, we can use emits(expectedData) to expect what a stream returns. And for Future, we can use completion(expectedData), But how about a return type of Query<T>? Please check my example below.
group('collectionQuery test', () {
final service = FirestoreService(firestore: fakeService);
test('expect to return the collection', () async {
for (var i = 0; i < testData.length; i++) {
final path = 'collections/id10$i';
await service.setData(path: path, data: testData[i].toMap());
}
final Query<Post> result = service.collectionQuery(
path: collectionPath,
fromMap: ((snapshot, options) =>
Post.fromMap(snapshot.data()!, snapshot.id)),
toMap: ((post, options) => post.toMap()));
expect(result, //whatIsTheExpectationHere(testData));
});
});
Here is the actual function under test:
Query<T> collectionQuery<T>({
required String path,
required T Function(DocumentSnapshot<Map<String, dynamic>> snapshot,
SnapshotOptions? options)
fromMap,
required Map<String, Object?> Function(T, SetOptions? options) toMap,
}) {
Query<T> query = firestore
.collection(path)
.withConverter<T>(fromFirestore: fromMap, toFirestore: toMap);
return query;
}
since type Query<T> doesnt have a matcher, another option that can be done is to convert it to Future/Stream in order to use the completion or emits matchers. In my case i converted into future and my test passed.
test('expect to return the collection', () async {
for (var i = 0; i < testData.length; i++) {
final path = 'collections/id10$i';
await service.setData(path: path, data: testData[i].toMap());
}
final Query<Post> result = service.collectionQuery(
path: collectionPath,
fromMap: ((snapshot, options) =>
Post.fromMap(snapshot.data()!, snapshot.id)),
toMap: ((post, options) => post.toMap()));
expect(result, isA<Query<Post>>());
final actualData = result.get().then((snapshot) {
final result = snapshot.docs
.map((snapshot) => snapshot.data())
.where((value) => value != null)
.toList();
return result;
});
expect(actualData, completion(testData));
});
I need to get 1 field 1 time from Firebase Cloud Firestore. How do I accomplish this with providers outside of Widget build?
Below are my combined providers. appStartupProvider is a FutureProvider and I want to get the bool value from this 1 field in firestore. However, the await in appStartupProvider states "'await' applied to 'AsyncValue', which is not a 'Future'".
final accountStreamProvider = StreamProvider<Account>((ref) {
final database = ref.watch(databaseProvider);
return database != null ? database.accountStream() : const Stream.empty();
});
final _accountSetupCompleteProvider = Provider<AsyncValue<bool>>((ref) {
return ref
.watch(accountStreamProvider)
.whenData((account) => account?.accountSetupComplete ?? false);
});
final appStartupProvider = FutureProvider<bool>((ref) async {
final accountSetupComplete = await ref.watch(_accountSetupCompleteProvider);
return accountSetupComplete;
});
Obviously missing some key knowledge here on combing providers and AsyncValue, but I'm trying to accomplish the situation stated on RiverPod Combining Providers page, where I see await is being used.
The example in the documentation was incorrect at the time of your post. It has since been updated and is now correct.
This is how you could write it:
final accountStreamProvider = StreamProvider<Account?>((ref) {
final database = ref.watch(databaseProvider);
return database != null ? database.accountStream() : const Stream.empty();
});
final _accountSetupCompleteProvider = FutureProvider<bool>((ref) async {
final account = await ref.watch(accountStreamProvider.last);
return account?.accountSetupComplete ?? false;
});
final appStartupProvider = FutureProvider<bool>((ref) async {
final accountSetupComplete = await ref.watch(_accountSetupCompleteProvider.future);
return accountSetupComplete;
});
Or:
final accountStreamProvider = StreamProvider<Account?>((ref) {
final database = ref.watch(databaseProvider);
return database != null ? database.accountStream() : const Stream.empty();
});
final _accountSetupCompleteProvider = Provider<AsyncValue<bool>>((ref) {
return ref
.watch(accountStreamProvider)
.whenData((account) => account?.accountSetupComplete ?? false);
});
final appStartupProvider = Provider<bool>((ref) {
final accountSetupComplete = ref.watch(_accountSetupCompleteProvider).maybeWhen(
data: (data) => data,
orElse: () => false,
);
return accountSetupComplete;
});
await usage is available via:
.future on FutureProvider documentation
.last on StreamProvider documentation
example
final carsListFutureProvider = FutureProvider<List<Car>>((ref) {
final backend = ref.watch(backendProvider);
return backend.getList(pathName, (json) => Car.fromJson(json));
});
final carFutureProvider = FutureProvider.family<Car?,int>((ref,id) async {
final list = await ref.watch(carsListFutureProvider.future);
return list.firstWhereOrNull((e) => e.id == id);
});
It seems that at the moment the documentation contains incorrect code examples. issue
I using Flutter Riverpod package to handling http request. I have simple Http get request to show all user from server, and i using manage it using FutureProvider from Flutter Riverpod package.
API
class UserGoogleApi {
Future<List<UserGoogleModel>> getAllUser() async {
final result = await reusableRequestServer.requestServer(() async {
final response =
await http.get('${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getAllUser');
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson['status'] == 'ok') {
final List list = responseJson['data'];
final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
return listUser;
} else {
throw responseJson['message'];
}
});
return result;
}
}
User Provider
class UserProvider extends StateNotifier<UserGoogleModel> {
UserProvider([UserGoogleModel state]) : super(UserGoogleModel());
Future<UserGoogleModel> searchUserByIdOrEmail({
String idUser,
String emailuser,
String idOrEmail = 'email_user',
}) async {
final result = await _userGoogleApi.getUserByIdOrEmail(
idUser: idUser,
emailUser: emailuser,
idOrEmail: idOrEmail,
);
UserGoogleModel temp;
for (var item in result) {
temp = item;
}
state = UserGoogleModel(
idUser: temp.idUser,
createdDate: temp.createdDate,
emailUser: temp.emailUser,
imageUser: temp.emailUser,
nameUser: temp.nameUser,
tokenFcm: temp.tokenFcm,
listUser: state.listUser,
);
return temp;
}
Future<List<UserGoogleModel>> showAllUser() async {
final result = await _userGoogleApi.getAllUser();
state.listUser = result;
return result;
}
}
final userProvider = StateNotifierProvider((ref) => UserProvider());
final showAllUser = FutureProvider.autoDispose((ref) async {
final usrProvider = ref.read(userProvider);
final result = await usrProvider.showAllUser();
return result;
});
After that setup, i simply can call showAllUser like this :
Consumer((ctx, read) {
final provider = read(showAllUser);
return provider.when(
data: (value) {
return ListView.builder(
itemCount: value.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
final result = value[index];
return Text(result.nameUser);
},
);
},
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('Error $error'),
);
}),
it's no problem if http request don't have required parameter, but i got problem if my http request required parameter. I don't know how to handle this.
Let's say , i have another http get to show specific user from id user or email user. Then API look like :
API
Future<List<UserGoogleModel>> getUserByIdOrEmail({
#required String idUser,
#required String emailUser,
#required String idOrEmail,
}) async {
final result = await reusableRequestServer.requestServer(() async {
final baseUrl =
'${appConfig.baseApiUrl}/${appConfig.userGoogleController}/getUserByIdOrEmail';
final chooseURL = idOrEmail == 'id_user'
? '$baseUrl?id_or_email=$idOrEmail&id_user=$idUser'
: '$baseUrl?id_or_email=$idOrEmail&email_user=$emailUser';
final response = await http.get(chooseURL);
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson['status'] == 'ok') {
final List list = responseJson['data'];
final listUser = list.map((e) => UserGoogleModel.fromJson(e)).toList();
return listUser;
} else {
throw responseJson['message'];
}
});
return result;
}
User Provider
final showSpecificUser = FutureProvider.autoDispose((ref) async {
final usrProvider = ref.read(userProvider);
final result = await usrProvider.searchUserByIdOrEmail(
idOrEmail: 'id_user',
idUser: usrProvider.state.idUser, // => warning on "state"
);
return result;
});
When i access idUser from userProvider using usrProvider.state.idUser , i got this warning.
The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'.
It's similiar problem with my question on this, but on that problem i already know to solved using read(userProvider.state) , but in FutureProvider i can't achieved same result using ref(userProvider).
I missed something ?
Warning: This is not a long-term solution
Assuming that your FutureProvider is being properly disposed after each use that should be a suitable workaround until the new changes to Riverpod are live. I did a quick test to see and it does work. Make sure you define a getter like this and don't override the default defined by StateNotifier.
class A extends StateNotifier<B> {
...
static final provider = StateNotifierProvider((ref) => A());
getState() => state;
...
}
final provider = FutureProvider.autoDispose((ref) async {
final a = ref.read(A.provider);
final t = a.getState();
print(t);
});
Not ideal but seems like a fine workaround. I believe the intention of state being inaccessible outside is to ensure state manipulations are handled by the StateNotifier itself, so using a getter in the meantime wouldn't be the end of the world.