The flutter package easy_localization has an extention method on the BuildContext which looks like this:
Locale get deviceLocale => EasyLocalization.of(this)!.deviceLocale;
I wish to mock this using Mocktail.
I tried:
import 'package:mocktail/mocktail.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:test/test.dart';
class MockBuildContext extends Mock implements BuildContext {}
void main() {
test("Sample test", () {
final context = MockBuildContext();
when(() => context.deviceLocale).thenReturn(const Locale('en', 'US'));
expect(Locale('en', 'US'), context.deviceLocale);
});
}
However this throws an error: type 'Locale' is not a subtype of type '_EasyLocalizationProvider?'. I am assuming this happens because we are (unsuccessfully) trying to mock an Extension method and that the original method is called instead.
How can I mock the extension method to get the desired result?
Related
I'm using injectable and get_it in flutter dart (following the very popular Reso coder)
I have a simple abstract class:
import 'package:injectable/injectable.dart';
//#injectable
abstract class IRandomQuantityRepository {
Future<int> getRandomQuantity();
}
and I have two simple concrete implementations of it:
import 'package:injectable/injectable.dart';
#dev
#injectable
class DevRandomQuantityRepository implements IRandomQuantityRepository {
const DevRandomQuantityRepository();
#override
Future<int> getRandomQuantity() async => 90;
}
and
import 'dart:math';
import 'package:injectable/injectable.dart';
#prod
#injectable
class RandomQuantityRepository implements IRandomQuantityRepository {
const RandomQuantityRepository();
#override
Future<int> getRandomQuantity() async => Random().nextInt(100);
}
Lastly, I have an injection.dart:
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'package:moontreeapp/injection.config.dart';
final GetIt getIt = GetIt.instance;
#InjectableInit(preferRelativeImports: false)
void configureInjection(String env) {
$initGetIt(getIt, environment: env);
}
abstract class Env {
static const prod = 'prod';
static const dev = 'dev';
}
besides all that I have a bloc that wants to use stuff:
#injectable
class RandomQuantityBloc
extends Bloc<RandomQuantityEvent, RandomQuantityState> {
final IRandomQuantityRepository _quantityFacade; // notice this final...
doesn't that look good? I think so. So then I run this command to make the generated code flutter pub run build_runner watch
But I get a message:
[RandomQuantityBloc] depends on unregistered type [IRandomQuantityRepository]... Did you forget to annotate the above class(s) or their implementation with #injectable?
or add the right environment keys?
Ok, so that's cool, lets add it to the interface:
import 'package:injectable/injectable.dart';
#injectable // <-- added
abstract class IRandomQuantityRepository {
Future<int> getRandomQuantity();
}
but then I get a new error:
> [IRandomQuantityRepository] is abstract and can not be registered directly!
> if it has a factory or a create method annotate it with #factoryMethod
> 14 │ abstract class IRandomQuantityRepository {
> │ ^^^^^^^^^^^^^^^^^^^^^^^^^
In the past I've handled dependency injection manually, so I'm new to these packages, what am I missing here?
Besides all that, the real issue is that I can't switch the injection based on the environment. I can use get_it to get a concrete dependency but not one based on environment like in this test:
/// has no effect:
configureInjection(Env.dev);
/// gets prod version:
final devRandomQuantity = getIt<RandomQuantityRepository>();
So something about this whole set up isn't configuring the Injections correctly... What am I missing?
One final thing that might be useful is to see the generated code:
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// InjectableConfigGenerator
// **************************************************************************
import 'package:get_it/get_it.dart' as _i1;
import 'package:injectable/injectable.dart' as _i2;
import 'package:moontreeapp/application/quantity/bloc/randomquantity_bloc.dart'
as _i5;
import 'package:moontreeapp/domain/quantity/i_randomquantity_repository.dart' as _i6;
import 'package:moontreeapp/infrastructure/quantity/dev_randomquantity_repository.dart'
as _i3;
import 'package:moontreeapp/infrastructure/quantity/mock_randomquantity_repository.dart'
as _i4;
import 'package:moontreeapp/infrastructure/quantity/randomquantity_repository.dart'
as _i7;
const String _dev = 'dev';
const String _prod = 'prod';
// ignore_for_file: unnecessary_lambdas
// ignore_for_file: lines_longer_than_80_chars
/// initializes the registration of provided dependencies inside of [GetIt]
_i1.GetIt $initGetIt(_i1.GetIt get,
{String? environment, _i2.EnvironmentFilter? environmentFilter}) {
final gh = _i2.GetItHelper(get, environment, environmentFilter);
gh.factory<_i3.DevRandomQuantityRepository>(
() => _i3.DevRandomQuantityRepository(),
registerFor: {_dev});
gh.factory<_i5.RandomQuantityBloc>(
() => _i5.RandomQuantityBloc(get<_i6.IRandomQuantityRepository>()));
gh.factory<_i7.RandomQuantityRepository>(() => _i7.RandomQuantityRepository(),
registerFor: {_prod});
return get;
}
Do I put #injectable on abstract classes or not?
ok, so I guess injectable can't see what the class implements so you have to make it explicit. also, I missed that #dev isn't built in, you have to make it.
So this is the proper way to use the decorations
#Environment('dev')
#Injectable(as: IRandomQuantityRepository)
class DevRandomQuantityRepository implements IRandomQuantityRepository {
const DevRandomQuantityRepository();
#override
Future<Either<QuantFailure, Quantity>> getRandomQuantity() async =>
right(Quantity(Amount(900.0001), Decimal(4)));
}
and that way this does work:
configureInjection(Env.dev);
final mockRandomQuantity = getIt<IRandomQuantityRepository>();
await mockRandomQuantity.getRandomQuantity();
// 90
I have this getx controller :
class CustomerPOAutocompleteState extends GetxController {
CustomerPOAutocompleteState();
...
Future<void> getCustomer(String title, String? propertyId) async {
var result = await RRGraphQL().artemis!...
_customers =...
update(['update']);
}
Now by Mock mocktail I created a mock class:
class MockController extends Mock implements CustomerPOAutocompleteState {}
When I try to put it I got error:
void main() {
late MockController mockController;
setUpAll(() {
mockController = MockController();
});
...
testWidgets('test', (WidgetTester tester) async {
Get.put<CustomerPOAutocompleteState>(mockController,tag: "");
error:
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following _TypeError was thrown running a test:
type 'Null' is not a subtype of type 'InternalFinalCallback<void>'
UPDATED Answer
This error occurs since Get.put performs calls on your GetxController which have not been mocked out correctly.
You can either do this manually (you will have to check the implementation details of Get.put and GetxController). Or you can do, what the Get documentation suggests and let your mock extend GetxController and use Mock as a mixin.
class MockController extends GetxController
with Mock
implements CustomerPOAutocompleteState {}
Check out this full example:
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:mocktail/mocktail.dart';
class DemoController extends GetxController {
var count = 0.obs;
increment() => count++;
}
class MockController extends GetxController
with Mock
implements DemoController {}
main() {
late MockController mockController;
setUpAll(() {
mockController = MockController();
});
test('GetX Test', () {
when(() => mockController.count).thenAnswer((_) => 12.obs);
Get.put<DemoController>(mockController, tag: "");
mockController.increment();
expect(mockController.count.value, 12);
});
}
I used url_launcher: ^6.1.0 in my flutter project.
I start to write tests for my widgets, but the part of widgets that used the url_launcher method to launch an URL, not worked properly when running the test.
One of the methods that I used inside my Widget is like below method:
Future<void> _onTapLink(String? href) async {
if (href == null) return;
// canLaunchUrl method never return anything when we are calling this function inside flutter test
if (await canLaunchUrl(Uri.parse(href))) {
await launchUrl(Uri.parse(href));
} else {
print('cannot launch url: $href');
}
}
canLaunchUrl method never returns anything when we are calling this function inside the flutter test.
I'm looking for a way to mock the url_launcher package for using inside
flutter tests.
To mock url_launcher you may:
Add plugin_platform_interface and url_launcher_platform_interface packages to dev_dependencies section in the pubspec.yaml file.
Implement the mock class. For example, with mocktail the implementation would be:
import 'package:mocktail/mocktail.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
class MockUrlLauncher extends Mock
with MockPlatformInterfaceMixin
implements UrlLauncherPlatform {}
Notice that here MockPlatformInterfaceMixin mixin is used.
Configure the mock as usual. With mocktail that might be:
MockUrlLauncher setupMockUrlLauncher() {
final mock = MockUrlLauncher();
registerFallbackValue(const LaunchOptions());
when(() => mock.launchUrl(any(), any())).thenAnswer((_) async => true);
return mock;
}
Tell url_launcher to use mocked version by setting it in UrlLauncherPlatform.instance:
final mock = setupMockUrlLauncher();
UrlLauncherPlatform.instance = mock;
This is an article that explicitly explains how to mock or fake the launchUrl function. Here is an example of how to mock it with mocktail. It also uses the ioc_container package to handle substitution with dependency injection.
import 'package:fafsdfsdf/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';
class LaunchMock extends Mock {
Future<bool> call(
Uri url, {
LaunchMode? mode,
WebViewConfiguration? webViewConfiguration,
String? webOnlyWindowName,
});
}
void main() {
testWidgets('Test Url Launch', (tester) async {
//These allow default values
registerFallbackValue(LaunchMode.platformDefault);
registerFallbackValue(const WebViewConfiguration());
//Create the mock
final mock = LaunchMock();
when(() => mock(
flutterDevUri,
mode: any(named: 'mode'),
webViewConfiguration: any(named: 'webViewConfiguration'),
webOnlyWindowName: any(named: 'webOnlyWindowName'),
)).thenAnswer((_) async => true);
final builder = compose()
//Replace the launch function with a mock
..addSingletonService<LaunchUrl>(mock);
await tester.pumpWidget(
builder.toContainer()<MyApp>(),
);
//Tap the icon
await tester.tap(
find.byIcon(Icons.favorite),
);
await tester.pumpAndSettle();
verify(() => mock(flutterDevUri)).called(1);
});
}
I am learning about TDD and practicing my test writing. I want to write a test for my login through Firebase.
here is the test file:
import 'package:firebase/firebase.dart';
import 'package:firebase_auth/firebase_auth.dart' as fa;
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:scopetik/features/login/data/login_data_impl.dart';
import 'package:scopetik/features/login/models/user_model.dart';
class MockFirebaseAuth extends Mock implements fa.FirebaseAuth {}
class MockFirebaseUser extends Mock implements fa.User {}
class MockAuthResult extends Mock implements fa.UserCredential {}
void main() {
late LoginDataImpl sut;
late MockFirebaseAuth mockFirebaseAuth;
setUp(() {
mockFirebaseAuth = MockFirebaseAuth();
sut = LoginDataImpl(mockFirebaseAuth);
});
group(
'Test LoginDataImpl class',
() {
when(mockFirebaseAuth.signInWithEmailAndPassword(
email: 'email', password: 'password'))
.thenAnswer((_) async {
return MockAuthResult();
});
test(
"get UserModel",
() async {
//arrange
},
);
},
);
}
I don't know why the when function won't let me return a future<UserCredential>.
I also had the same problem. You need to use mockito instead of mocktail in order to do that.
You can find when property of both libraries here, mocktail, mockito
In my app I am using sembast db and I'm trying to adapt the AppDatabase singleton class to be platform dependent and use sambast_web when app is running on the web.
I'm trying to use the same pattern I used for user location so I created:
A stub with a getter method.
An abstract class AppDatabase to have a conditional package import for the device and web packages when importing the stub , and in the class factory method returns the stub getter method.
A class AppDatabaseDevice and a class AppDatabaseWeb which both implement AppDatabase.
When running the app dough I get an error:
Compiler message:
lib/fixit_shop_app/database/app_database_switcher.dart:50:28: Error: Method not found: 'getAppDatabase'.
factory AppDatabase() => getAppDatabase();
^^^^^^^^^^^^^^
Target kernel_snapshot failed: Exception: Errors during snapshot creation: null
Failed to build bundle.
Error launching application on iPad (6th generation).
Now, it's quite strange as when writing the code for the abstract class factory it finds getAppDatabase() an throws no error.
After twiddling a bit with the code I nailed down the problem being the conditional import of the stub import.
If I import it without conditional import as
import 'package:fixit_shop_flutter/fixit_shop_app/database/app_database_stub.dart';
then I get no error, but then again I do need the conditional import..
Can you spot why conditional import is failing for this class and works for the other?
As always thank you very much for your time and help.
These are the the AppDatabase methods:
Stub:
import 'app_database_switcher.dart';
AppDatabase getAppDatabase() => throw UnsupportedError(
'Cant get AppDatabase if not loading the right package');
Abstract class:
import 'dart:async';
import 'package:sembast/sembast.dart';
import 'app_database_stub.dart'
if (dart.library.io) 'package:sembast/sembast.dart'
if (dart.library.js) 'package:sembast_web/sembast_web.dart';
abstract class AppDatabase {
// Singleton instance
static final AppDatabase _singleton;
// Singleton accessor
static AppDatabase get instance => _singleton;
// Completer is used for transforming synchronous code into asynchronous code.
Completer<Database> _dbOpenCompleter;
// A private constructor. Allows us to create instances of AppDatabase
// only from within the AppDatabase class itself.
// Sembast database object
Database _database;
// Database object accessor
Future<Database> get database async {
// // If completer is null, AppDatabaseClass is newly instantiated, so database is not yet opened
return _dbOpenCompleter.future;
}
factory AppDatabase() => getAppDatabase();
}
And this are user location methods:
Stub:
import 'package:fixit_shop_flutter/fixit_shop_app/platform_user_location/user_location_switcher.dart';
UserLocation getUserLocation() =>
throw UnsupportedError('user_location_stub error');
Abstract class:
import 'package:fixit_shop_flutter/fixit_shop_app/platform_user_location/user_location_stub.dart' // Version which just throws UnsupportedError
if (dart.library.io) "package:fixit_shop_flutter/fixit_shop_app/platform_user_location/user_location_device.dart"
if (dart.library.js) "package:fixit_shop_flutter/fixit_shop_app/platform_user_location/user_location_web.dart";
abstract class UserLocation {
// methods to be used
Future<Map<String, dynamic>> getPosition() async {
Future<Map<String, dynamic>> position;
return position;
}
factory UserLocation() => getUserLocation();
}
Found the error. Indeed it was the conditional import. I was importing the diverse sembast packages instead of the device and web class implementations..
import 'package:fixit_shop_flutter/fixit_shop_app/database/app_database_stub.dart'
if (dart.library.io) 'package:fixit_shop_flutter/fixit_shop_app/database/app_database_device.dart'
if (dart.library.js) 'package:fixit_shop_flutter/fixit_shop_app/database/app_database_web.dart';