Mocking GetStorage for testing in flutter - flutter

I am unit testing and widget testing my code. I have tried mokito and moktail to mock the Get storage but get this error:
package:get_storage/src/storage_impl.dart 47:7 GetStorage._init
===== asynchronous gap ===========================
package:get_storage/src/storage_impl.dart 28:7 new GetStorage._internal.<fn>
the class that I am testing:
class ShowCaseController extends GetxController {
final box = GetStorage();
displayAnySC(String playKey, String replayKey, GetStorage box) async {
bool? showcasePlayStatus = box.read(playKey);
if (showcasePlayStatus == null) {
box.write(playKey, false);
// box.remove(replayKey);
box.write(replayKey, false);
return true;
}
bool? showcaseReplayStatus = box.read(replayKey);
if (showcaseReplayStatus == null) {
box.write(replayKey, false);
return true;
}
return false;
}
}
here is one empty simple test using mock that gives error:
class MockStorage extends Mock implements GetStorage {}
void main() {
group('Show case controller', () {
final showCaseCnt = ShowCaseController();
late bool _data;
late MockStorage _mockStorage;
setUp(() {
_mockStorage = MockStorage();
_data = showCaseCnt.displayAnySC('playKey', 'replayKey', _mockStorage);
});
test(
'displayAnySC should return false when the play and replay are not null',
() {
});
});
}
I have tried #GenerateMocks([ShowCaseController]) and also added GetStorage.init() inside the main function of the test but got the same error.
P.S. I haven't seen any article or question related to mocking the GetStorage for test in Flutter. Appreciate any explanation or link that helps in this regard.
I am using the GetX package for dependency injection and state management. And using the GetStorage package for keeping the theme persistent and storing keys for notifying the app to play or replay ShowCaseView.

Related

Provider: How to notify listeners of A from B

I have two controllers using the changenotifiers in my app and I want to notify listeners of A from B. I have be trying two different methods for the same.
Method 1
(Controller A):
class DrawController extends ChangeNotifier {
void clearCanvas() {
selectedText = '';
shouldClearCanvas = true;
currentDrawPoints.initialPoint = null;
currentDrawPoints.endPoint = null;
print(currentDrawPoints.initialPoint);
notifyListeners();
}
}
Calling it from (Controller B):
class GameController extends ChangeNotifier {
DrawController drawController = DrawController();
void someFunction(){
drawController.clearCanvas();
notifyListeners();
}
}
The above method doesn't work. It does execute the function and changes the value but it does not change the UI (does not rebuild the widget).
Method 2:
(Controller A):
class DrawController extends ChangeNotifier {
void clearCanvas() {
selectedText = '';
shouldClearCanvas = true;
currentDrawPoints.initialPoint = null;
currentDrawPoints.endPoint = null;
print(currentDrawPoints.initialPoint);
notifyListeners();
}
}
Calling it from (Controller B):
class GameController extends ChangeNotifier {
DrawController drawController = DrawController();
void someFunction(BuildContext context){
Provider.of<DrawController>(context, listen: false).clearCanvas();
}
}
This second method works and it updates the UI too but I think this is the wrong method of doing it.
As using it after async await gives the warning "Do not use BuildContexts across async gaps."
class GameController extends ChangeNotifier {
DrawController drawController = DrawController();
void someFunction(BuildContext context) async {
User? userData = await localStorage.getUserData();
Provider.of<DrawController>(context, listen: false)
.clearCanvas(); //Gives warning "Do not use BuildContexts across async gaps."
}
}
Can anyone tell me the correct way of doing it.

Flutter GetX state management initial null value

This is what I'm trying to achieve using flutter GetX package but not working properly.
I have a Firestore document, if the document is changed I want to call an api and keep the data up to date as observable.
The code below seems to work but initial screen shows null error then it shows the data.
I don't know how I can make sure both fetchFirestoreUser() and fetchApiData() (async methods) returns data before I move to the home screen.
GetX StateMixin seems to help with async data load problem but then I don't know how I can refresh the api data when the firestore document is changed.
I'm not sure if any other state management would be best for my scenario but I find GetX easy compared to other state management package.
I would very much appreciate if someone would tell me how I can solve this problem, many thanks in advance.
Auth Controller.
class AuthController extends SuperController {
static AuthController instance = Get.find();
late Rx<User?> _user;
FirebaseAuth auth = FirebaseAuth.instance;
var _firestoreUser = FirestoreUser().obs;
var _apiData = ProfileUser().obs;
#override
void onReady() async {
super.onReady();
_user = Rx<User?>(auth.currentUser);
_user.bindStream(auth.userChanges());
//get firestore document
fetchFirestoreUser();
//fetch data from api
fetchApiData();
ever(_user, _initialScreen);
//Refresh api data if firestore document has changed.
_firestoreUser.listen((val) {
fetchApiData();
});
}
Rx<FirestoreUser?> get firestoreUser => _firestoreUser;
_initialScreen(User? user) {
if (user == null) {
Get.offAll(() => Login());
} else {
Get.offAll(() => Home());
}
}
ProfileUser get apiData => _apiData.value;
void fetchFirestoreUser() async {
Stream<FirestoreUser> firestoreUser =
FirestoreDB().getFirestoreUser(_user.value!.uid);
_firestoreUser.bindStream(firestoreUser);
}
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
#override
void onDetached() {}
#override
void onInactive() {}
#override
void onPaused() {}
#override
void onResumed() {
fetchApiData();
}
}
Home screen
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Obx(() =>
Text("username: " + AuthController.instance.apiData.username!))),
),
);
}
}
To be honest, I never used GetX so I'm not too familiar with that syntax.
But I can see from your code that you're setting some mutable state when you call this method:
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
Instead, a more robust solution would be to make everything reactive and immutable. You could do this by combining providers if you use Riverpod:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
final authService = ref.watch(authRepositoryProvider);
return authService.authStateChanges();
});
final apiDataProvider = FutureProvider.autoDispose<APIData?>((ref) {
final userValue = ref.watch(authStateChangesProvider);
final user = userValue.value;
if (user != null) {
// note: this should also be turned into a provider, rather than using a static method
return RemoteService.getProfile(user.uid);
} else {
// decide if it makes sense to return null or throw and exception when the user is not signed in
return Future.value(null);
}
});
Then, you can just use a ConsumerWidget to watch the data:
#override
Widget build(BuildContext context, WidgetRef ref) {
// this will cause the widget to rebuild whenever the auth state changes
final apiData = ref.watch(apiDataProvider);
return apiData.when(
data: (data) => /* some widget */,
loading: () => /* some loading widget */,
error: (e, st) => /* some error widget */,
);
}
Note: Riverpod has a bit of a learning curve (worth it imho) so you'll have to learn it how to use it first, before you can understand how this code works.
Actually the reason behind this that you put your controller in the same page that you are calling so in the starting stage of your page Get.put() calls your controller and because you are fetching data from the API it takes a few seconds/milliseconds to get the data and for that time your Obx() renders the error. To prevent this you can apply some conditional logic to your code like below :
Obx(() => AuthController.instance.apiData != null ? Text("username: " + AuthController.instance.apiData.username!) : CircularProgressIndicator())) :

Use a specific instance of a class inside an isolate

I am using an isolate through the compute() method to fetch, parse and sort datas from an API (around 10k entries).
My method getAllCards() is defined inside a class YgoProRepositoryImpl which has an instance of my remote datasource class YgoProRemoteDataSource it is in this class that the method to call my API is defined (it is a simple GET request).
Code Sample
ygopro_repository_impl.dart
class YgoProRepositoryImpl implements YgoProRepository {
final YgoProRemoteDataSource remoteDataSource;
// ...
YgoProRepositoryImpl({
required this.remoteDataSource,
// ...
});
// ...
static Future<List<YgoCard>> _fetchCards(_) async {
// As I'm inside an isolate I need to re-setup my locator
setupLocator();
final cards = await sl<YgoProRemoteDataSource>()
.getCardInfo(GetCardInfoRequest(misc: true));
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, null);
return cards;
}
// ...
}
service_locator.dart
import 'package:get_it/get_it.dart';
import 'data/api/api.dart';
import 'data/datasources/remote/ygopro_remote_data_source.dart';
import 'data/repository/ygopro_repository_impl.dart';
import 'domain/repository/ygopro_repository.dart';
final sl = GetIt.instance;
void setupLocator() {
// ...
_configDomain();
_configData();
// ...
_configExternal();
}
void _configDomain() {
//! Domain
// ...
// Repository
sl.registerLazySingleton<YgoProRepository>(
() => YgoProRepositoryImpl(
remoteDataSource: sl(),
// ...
),
);
}
void _configData() {
//! Data
// Data sources
sl.registerLazySingleton<YgoProRemoteDataSource>(
() => YgoProRemoteDataSourceImpl(sl<RemoteClient>()),
);
// ...
}
void _configExternal() {
//! External
sl.registerLazySingleton<RemoteClient>(() => DioClient());
// ...
}
The code is working properly but getAllCards() is not testable as I cannot inject a mocked class of YgoProRemoteDataSource inside my isolate because it will always get a reference from my service locator.
How can I do to not rely on my service locator to inject YgoProRemoteDataSource inside my isolate and make getAllCards() testable ?
Did a more serious attempt, please see the repo: https://github.com/maxim-saplin/compute_sl_test_sample
Essentially with the current state of affairs with Flutter/Dart you can't pass neither closures nor classes containing closures across isolates boundaries (yet that might change when newer features in Dart land Flutter https://github.com/dart-lang/sdk/issues/46623#issuecomment-916161528). That means there's no way you can pass service locator (which contains closures) or trick the isolate to instantiate a test version of locator via closure IF you don't want any test code to be part of the release build. Yet you can easily pass data source instance to isolate to be used at its entry point as a param.
Beside, I don't think asking isolate to rebuild the entire service locator makes sense. The whole idea behind compute() is to create a short leaving isolate, run the computation, return the result and terminate the isolate. Initialising the locator is an overhead which is better to be avoided. Besides it seems the whole concept of compute() is being as isolated from the rest of the app as possible.
You can clone the repo and run the tests. Few words about the sample:
Based on Flutter counter starter app
lib/classes.dart recreates the code snippet you provided
test/widget_test.dart verifies that YgoProRepositoryImpl is working fine with isolate running fake version of data source
YgoProRemoteDataSourceImpl mimics real implementation and is located at classes.dart and YgoProRemoteDataSourceFake mimics test version
Running isolates under flutter_test requires wrapping test body in tester.runAsync() in order to have real time async execution (rather than fake async used by default by tests and relying on pumping to progress test time). Running tests in this mode can be slow (there's actual 0.5 second wait), structuring the tests in a way when compute() is not used or tested not in many tests is reasonable
classes.dart
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
final sl = GetIt.instance;
class YgoCard {
YgoCard(this.name);
final String name;
}
abstract class YgoProRemoteDataSource {
Future<List<YgoCard>> getCardInfo();
}
class YgoProRemoteDataSourceImpl extends YgoProRemoteDataSource {
#override
Future<List<YgoCard>> getCardInfo() {
return Future.delayed(Duration.zero,
() => List.generate(5, (index) => YgoCard("Impl $index")));
}
}
abstract class YgoProRepository {
Future<List<YgoCard>> getAllCards();
}
class YgoProRepositoryImpl implements YgoProRepository {
final YgoProRemoteDataSource remoteDataSource;
YgoProRepositoryImpl({
required this.remoteDataSource,
});
static Future<List<YgoCard>> _fetchCards(
YgoProRemoteDataSource dataSource) async {
final cards = await dataSource.getCardInfo();
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, remoteDataSource);
return cards;
}
}
void setupLocator() {
sl.registerLazySingleton<YgoProRepository>(
() => YgoProRepositoryImpl(
remoteDataSource: sl(),
),
);
sl.registerLazySingleton<YgoProRemoteDataSource>(
() => YgoProRemoteDataSourceImpl(),
);
}
widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:test_sample/classes.dart';
import 'package:test_sample/main.dart';
void main() {
setUpAll(() async {
setupFakeLocator();
});
testWidgets('Test mocked data source', (WidgetTester tester) async {
// Wrapping with runAync() is required to have real async in place
await tester.runAsync(() async {
await tester.pumpWidget(const MyApp());
// Let the isolate spawned by compute() complete, Debug run might require longer wait
await Future.delayed(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
expect(find.text('Fake 9'), findsOneWidget);
});
});
}
class YgoProRemoteDataSourceFake extends YgoProRemoteDataSource {
#override
Future<List<YgoCard>> getCardInfo() {
return Future.delayed(Duration.zero,
() => List.generate(10, (index) => YgoCard("Fake $index")));
}
}
void setupFakeLocator() {
sl.registerLazySingleton<YgoProRepository>(
() => YgoProRepositoryImpl(
remoteDataSource: sl(),
),
);
sl.registerLazySingleton<YgoProRemoteDataSource>(
() => YgoProRemoteDataSourceFake(),
);
}
Do you really need to test the getCards() function?
What are you really testing there? That compute works, sure hope the Dart SDK team has a test for this.
That leaves _fetchCards(), and setupLocator() doesn't need to be tested either, it is precondition for your test-logic. You want to change the setup for the test anyways.
So what you actually want to test is the fetching & sorting. Restructure this into a testable static function and setup your locator beforehand. Put a #visibleForTesting annotation on it.
And on a side-note, depending on how much you bind in your service locator, this could be huge overhead for just using the one repository afterwards.
Example:
static Future<List<YgoCard>> _fetchCards(_) async {
// As I'm inside an isolate I need to re-setup my locator
setupLocator();
return reallyFetchCards();
}
#visibleForTesting
static Future<List<YgoCard>> reallyFetchCards() async {
final cards = await sl<YgoProRemoteDataSource>()
.getCardInfo(GetCardInfoRequest(misc: true));
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, null);
return cards;
}
Test:
// Setup SL and datasource
...
final cards = await YgoProRepositoryImpl.reallyFetchCrads();
// Expect stuff
As I understand you have two options, either inject the dependencies needed for static Future<List<YgoCard>> _fetchCards(_) async via parameters, or mock the object in the locator itself. I would go for the fist option, and have something like :
static Future<List<YgoCard>> _fetchCards(_,YgoProRemoteDataSource remote) async {
// No need to set up locator as you passed the needed dependencies
// setupLocator();
final cards = await remote
.getCardInfo(GetCardInfoRequest(misc: true));
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, null);
return cards;
}
Edit
just updated the answer as its easier to edit this here than in the comments...
Hmm, the only workaround that I can think of is to pass the setupLocator() function as an argument to the class YgoProRepositoryImpl :
final Function setupLocator;
YgoProRepositoryImpl({
required this.remoteDataSource,
required this.setupLocator;
// ...
});
This way you could pass a mock that sets up your mock classes or the real setupLocator of your service_locator.dart. This might not be to elegant. But it should make it testable as now you can mock the setup and its not hardcoded in the function

Flutter GetX Provider & GetConnect Settings

I created a Flutter project with GetX CLI. Then i created a provider with get generate model. But i have problems with provider:
When i bind it with lazyPut, onInit() never works.
When i bind it with put, onInit() works but httpClient.defaultDecoder section does
not executed.
When i bind it with put, onInit() works and httpClient.baseUrl is adjusted but i can't read it in getFaculties. It seems like null in global.
When i put httpClient.defaultDecoder and httpClient.baseUrl in getFaculties, works well. But doesn't seem like the right way.
Am i doing something wrong or is it about GetConnect? Thanks in advance.
Provider:
class FacultyProvider extends GetConnect {
#override
void onInit() {
print('PROVIDER INIT!'); // Works with Get.put(FacultyProvider()) in Bindings
httpClient.defaultDecoder = (map) { // NEVER WORKS
if (map is Map<String, dynamic>) {
print('MAP!');
return Faculty.fromJson(map);
}
if (map is List) {
print('LIST!');
return map.map((item) => Faculty.fromJson(item)).toList();
}
};
httpClient.baseUrl = 'http://localhost:3000/public';
}
Future<List<Faculty>> getFaculties(bool cache) async {
print(httpClient.baseUrl); // ALWAYS NULL
final response = await httpClient.get('/faculties/true'); // NO HOST ERROR
return response.body; // TYPE ERRORS because of defaultDecoder section didn't run
}
Bindings:
class FacultiesBinding extends Bindings {
#override
void dependencies() {
Get.put(FacultyProvider());
// Get.lazyPut<FacultyProvider>(() => FacultyProvider());
Get.lazyPut<FacultiesController>(() => FacultiesController());
}

Can Provider handle state of a complexed class such as this? If not, how can I make this class available to my entire app in its global state?

I would like make a Currency object (shown below) shared throughout my entire app using global state (perhaps this isn't the best way?).
I have tried using the Provider package but I cannot seem to get it working.
class Currency extends ChangeNotifier {
String baseCurrency = 'USD';
Map<String, double> rates = {};
List<String> currencList = [];
Future<Map<dynamic, dynamic>> getLatestCurrencyData() async {
NetworkHelper networkHelper = NetworkHelper('$url_fixer_io$api_key');
var currencyData = await networkHelper.getData();
return currencyData;
}
void populateRates() async {
Map<dynamic, dynamic> currencyData = await getLatestCurrencyData();
Map<String, dynamic> newRates = currencyData['rates'];
newRates.forEach((curr, rate) {
this.currencList.add(curr);
this.rates.putIfAbsent(curr, () => rate.toDouble());
});
notifyListeners();
}
Future<List<String>> getCurrencyList() async {
if (currencList.length == 0) {
await populateRates();
}
return currencList;
}
void changeBaseCurrency(String newCurrency) {
baseCurrency = newCurrency;
notifyListeners();
}
}
The above implementation gives me the following error:
flutter: Another exception was thrown: Tried to use Provider with a subtype of Listenable/Stream (Currency).
I have limited experience managing state on reactive platforms such as flutter, and any tips on how I could accomplish this would be very much appreciated!
It does not matter how complex your class is.
If your class extends ChangeNotifier
Then your class should be made available to the tree by using either
ChangeNotifierProvider
or
ListenableProvider
My guess is you were using Provider instead of this two, which has an assertion set to throw if used with Listenable or Stream(ChangeNotifer is Listenable)
This is a portion of code from Provider class
assert((){
if (value is Listenable || value is Stream) {
throw FlutterError(''')
Instead of Provider use ChangeNotifierProvider or ListenableProvider. The difference is that ChangeNotifierProvider automatically calls the dispose method while you override the dispose method in ListenableProvider