I am using mockito 4.1.3 , and here I have some test class:
import 'package:flutter_test/flutter_test.dart';
import 'package:ghinbli_app/models/film_model.dart';
import 'package:ghinbli_app/network/ghibli_films.dart';
import 'package:mockito/mockito.dart';
class MockClient extends Mock implements GhibliFilms {
#override
Future<List<FilmModel>> getFilms() async{
return null;
}
}
void main() {
final GhibliFilms ghibliMock = MockClient();
test('If API call was unsuccessful and data received is null', () {
expect(ghibliMock.getFilms(), null);
});
}
Inside the MockClient class, I am overriding a method called getFilms() and returning null to simulate a situation when a call to some API returns null as data.
A problem
When I try to check if getFilms() actually returns a null value my test will fail with this error (probably because of the return type of getFilms()):
Expected: <null>
Actual: <Instance of 'Future<List<FilmModel>>'>
How can I check and test that the data from getFilms() is actually null, what am I doing wrong?
I've tested your code and got same error as you. After making these changes everything runs fine, try it yourself.
class MockClient extends Mock implements GhibliFilms {
#override
Future<List<FilmModel>> getFilms() async {
return Future.value(null); // this is not that important
}
}
void main() {
final GhibliFilms ghibliMock = MockClient();
// async/await here was important
test('If API call was unsuccessful and data received is null', () async {
expect(await ghibliMock.getFilms(), null);
});
}
Related
How can I mock a function in flutter and verify it has been called n times?
Ive tried implementing Mock from mockito but it only throws errors:
class MockFunction extends Mock {
call() {}
}
test("onListen is called once when first listener is registered", () {
final onListen = MockFunction();
// Throws: Bad state: No method stub was called from within `when()`. Was a real method called, or perhaps an extension method?
when(onListen()).thenReturn(null);
bloc = EntityListBloc(onListen: onListen);
// If line with when call is removed this throws:
// Used on a non-mockito object
verify(onListen()).called(1);
});
});
As a workaround I am just manually tracking the calls:
test("...", () {
int calls = 0;
bloc = EntityListBloc(onListen: () => calls++);
// ...
expect(calls, equals(1));
});
So is there a way I can create simple mock functions for flutter tests?
What you could do is this:
class Functions {
void onListen() {}
}
class MockFunctions extends Mock implements Functions {}
void main() {
test("onListen is called once when first listener is registered", () {
final functions = MockFunctions();
when(functions.onListen()).thenReturn(null);
final bloc = EntityListBloc(onListen: functions.onListen);
verify(functions.onListen()).called(1);
});
}
The accepted answer is correct, but it doesn't represent a real-life scenario where you will probably want to substitute a top-level function with a mock or a fake. This article explains how to include top-level functions in your dependency injection composition so that you can substitute those functions with mocks.
You can compose dependency injection like this and point to top-level functions such as launchUrl with ioc_container.
IocContainerBuilder compose() => IocContainerBuilder(
allowOverrides: true,
)
..addSingletonService<LaunchUrl>(
(url, {mode, webOnlyWindowName, webViewConfiguration}) async =>
launchUrl(
url,
mode: mode ?? LaunchMode.platformDefault,
webViewConfiguration:
webViewConfiguration ?? const WebViewConfiguration(),
webOnlyWindowName: webOnlyWindowName,
),
)
..add((container) => MyApp(launchUrl: container<LaunchUrl>()));
Then, you can use the technique mentioned in the answer here to mock with Mocktail.
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 trying to create a persistent interface which forks db calls to floor or another self made web db static store.
Anyway...
The interface part is looking like this:
peristent_interface.dart
import 'package:flutter/material.dart';
import 'package:mwork/database/floor/entities/map_location_entity.dart';
import 'package:mwork/database/floor/result/map_location_result.dart';
import 'persistent_stub.dart'
if(dart.library.io) 'persistent_native.dart'
if(dart.library.js) 'persistent_web.dart';
abstract class Persistent extends ChangeNotifier {
static Persistent? _instance;
static Persistent? get instance{
_instance ??= getPersistent();
return _instance;
}
Future<List<MapLocationResult?>?> getMapLocations();
Future<MapLocationResult?> getMapLocation({int id});
Future<void> insertReplaceMapLocation(MapLocation mapLocation);
Future<void> insertReplaceMapLocations(List<MapLocation> mapLocations);
}
All seems nice so far, but the trouble appears when the init() function below returns Future<AppDatabase> not AppDatabase as I want.
persistent_native.dart
import 'package:floor/floor.dart';
import 'package:mwork/database/floor/database/database.dart';
import 'package:mwork/database/floor/entities/map_location_entity.dart';
import 'package:mwork/database/floor/result/map_location_result.dart';
import 'package:mwork/services/persistent/persistent_interface.dart';
import 'package:mwork/common/m_work_config.dart' as m_work_config;
Persistent getPersistent() => PersistentNative();
class PersistentNative extends Persistent {
final AppDatabase _appDatabase = init(); //<-- Fails here !!
static Future<AppDatabase> init() async {
return await $FloorAppDatabase.databaseBuilder(m_work_config.mWorkFloorDb).build();
}
#override
Future<List<MapLocationResult?>?> getMapLocations() async {
return await _appDatabase.mapLocationDao.getMapLocations();
}
#override
Future<MapLocationResult?> getMapLocation({int id=-1}) async {
return await _appDatabase.mapLocationDao.getMapLocation(id);
}
#override
Future<void> insertReplaceMapLocation(MapLocation mapLocation) async {
_appDatabase.mapLocationDao.insertMapLocation(
mapLocation
);
}
#override
Future<void> insertReplaceMapLocations(List<MapLocation> mapLocations) async {
_appDatabase.mapLocationDao.insertMapLocations(
mapLocations
);
}
}
How should I return AppDatabase from init() ?
Maybe you should change the type of the init() function to AppDatabase instead of Future<AppDatabase>? For me it seems that the code is right one and should return AppDatabase.
The init method returns a future, since you wait for it ( and it is a recommended way)
if you would like to return the AppDatabase only, rewrite it as follows::
static AppDatabase init() {
return $FloorAppDatabase.databaseBuilder(m_work_config.mWorkFloorDb).build().then((AppDatabase db) => db);}
Doing this will have some implications though, this wont be awaited meaning that any call depending on this would return late..
I'd recommend using an await clause to the callee,
for example
static Future<AppDatabase> init() async {
return await $FloorAppDatabase.databaseBuilder(m_work_config.mWorkFloorDb).build();
}
and then calling it as::
final AppDatabase db = await (...........);
or:::
YourClass.init().then((AppDatabase db) { /* anything here*/});
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
what is the correct approach to test riverpod with mockito?
running the code above,
/// ### edited snippets from production side ###
/// not important, skip to the TEST below!
/// this seems meaningless just because it is out of context
mixin FutureDelegate<T> {
Future<T> call();
}
/// delegate implementation
import '../../shared/delegate/future_delegate.dart';
const k_STRING_DELEGATE = StringDelegate();
class StringDelegate implements FutureDelegate<String> {
const StringDelegate();
#override
Future<String> call() async {
/// ... returns a string at some point, not important now
}
}
/// the future provider
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '<somewhere>/delegate.dart'; /// the code above
final stringProvider = FutureProvider<String>((ref) => k_STRING_DELEGATE());
/// ### edited snippets from TEST side ###
/// mocking the delegate
import 'package:mockito/mockito.dart';
import '<see above>/future_delegate.dart';
class MockDelegate extends Mock implements FutureDelegate<String> {}
/// actual test
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/all.dart';
import 'package:mockito/mockito.dart';
import '<somewhere in my project>/provider.dart';
import '../../domain/<somewhere>/mock_delegate.dart'; // <= the code above
void main() {
group('`stringProvider`', () {
final _delegate = MockDelegate();
test('WHEN `delegate` throws THEN `provider`return exception',
() async {
when(_delegate.call()).thenAnswer((_) async {
await Future.delayed(const Duration(seconds: 1));
throw 'ops';
});
final container = ProviderContainer(
overrides: [
stringProvider
.overrideWithProvider(FutureProvider((ref) => _delegate()))
],
);
expect(
container.read(stringProvider),
const AsyncValue<String>.loading(),
);
await Future<void>.value();
expect(container.read(stringProvider).data.value, [isA<Exception>()]);
});
});
}
running the test returns
NoSuchMethodError: The getter 'value' was called on null.
Receiver: null
Tried calling: value
dart:core Object.noSuchMethod
src/logic/path/provider_test.dart 28:48 main.<fn>.<fn>
I'm new to riverpod, clearly I'm missing something
I tried to follow this
I found that I had some extra errors specifically when using StateNotifierProvider. The trick was to not only override the StateNotifierProvider, but also its state property (which is a StateNotifierStateProvider object).
class SomeState {
final bool didTheThing;
SomeState({this.didTheThing = false});
}
class SomeStateNotifier extends StateNotifier<SomeState> {
SomeStateNotifier() : super(SomeState());
bool doSomething() {
state = SomeState(didTheThing: true);
return true;
}
}
final someStateProvider = StateNotifierProvider<SomeStateNotifier>((ref) {
return SomeStateNotifier();
});
class MockStateNotifier extends Mock implements SomeStateNotifier {}
void main() {
final mockStateNotifier = MockStateNotifier();
when(mockStateNotifier.doSomething()).thenReturn(true);
final dummyState = SomeState(didTheThing: true); // This could also be mocked
ProviderScope(
overrides: [
someStateProvider.overrideWithValue(mockStateProvider), // This covers usages like "useProvider(someStateProvider)"
someStateProvider.state.overrideWithValue(dummyState), // This covers usages like "useProvider(someStateProvider.state)"
],
child: MaterialApp(...),
);
}
There are 2 errors in your code
You're trying to test a throw error, so you should use thenThrow instead of thenAnswer, but because you're overriding a mixing method I would recommend instead of using Mock use Fake (from the same mockito library) to override methods and then throw it as you want
class MockDelegate extends Fake implements FutureDelegate<String> {
#override
Future<String> call() async {
throw NullThrownError; //now you can throw whatever you want
}
}
And the second problem (and the one your code is warning you) is that you deliberately are throwing, so you should expect an AsyncError instead, so calling container.read(stringProvider).data.value is an error because reading the riverpod documentation:
When calling data:
The current data, or null if in loading/error.
so if you're expecting an error (AsyncError) data is null, and because of that calling data.value its the same as writing null.value which is the error you're experiencing
This is the code you could try:
class MockDelegate extends Fake implements FutureDelegate<String> {
#override
Future<String> call() async {
throw NullThrownError;
}
}
void main() {
group('`stringProvider`', () {
final _delegate = MockDelegate();
test('WHEN `delegate` throws THEN `provider`return exception', () async {
final container = ProviderContainer(
overrides: [
stringProvider
.overrideWithProvider(FutureProvider((ref) => _delegate.call()))
],
);
expect(container.read(stringProvider), const AsyncValue<String>.loading());
container.read(stringProvider).data.value;
await Future<void>.value();
expect(container.read(stringProvider), isA<AsyncError>()); // you're expecting to be of type AsyncError because you're throwing
});
});
}
Also consider mocking out various providers by using an Override in your top level ProviderScope. That's what override can do quite well.
Currently I'm following a tutorial of Resocoder (DDD Course). In this course there is an abstract class called i_auth_facade.dart, which contains a function getSignedInUser.
I have mocked the class and trying to get a valid option of user back from the getSignedInUser method, but the result is always null.
Also when I try to execute the authCheckRequested method of the AuthBloc, which then in the background invokes the getSignedInUser method, I get back null.
I must have some error with Mockito, as it seems the stub doesn't work.
Can anybody point me into the right direction why I don't get back the optionOf(User), but null instead?
import 'package:bloc_test/bloc_test.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:teach_mob_student/application/auth/auth_bloc.dart';
import 'package:teach_mob_student/domain/auth/i_auth_facade.dart';
import 'package:teach_mob_student/domain/auth/user.dart';
import 'package:teach_mob_student/domain/auth/value_objects.dart';
import 'package:teach_mob_student/domain/core/value_objects.dart';
class MockIAuthFacade extends Mock implements IAuthFacade {}
void main() {
final tValidUser = User(
id: UniqueId(),
name: StringSingleLine('Test User'),
emailAddress: EmailAddress('test#user.com'));
test('Should return valid option of user', () async {
// This is to check, if optionOf actually returns a user --> Yes it does
Option<User> tDirectResult = optionOf<User>(tValidUser);
// Here I stub the getSignedInUser method. If it is executed, I want to receive a valid user.
when(MockIAuthFacade()
.getSignedInUser()
).thenAnswer((_) async => Future.value(optionOf<User>(tValidUser)));
// Here I execute the method, and I would expect to get Future<Option<User>> back, but I only get back null.
final facade = MockIAuthFacade();
Option<User> tStubbedResult = await facade.getSignedInUser();
print(tDirectResult);
print(tStubbedResult);
});
// Here I do the same, but I execute the authCheckRequested method of the AuthBloc, which executes the
// getSignedInUser method. Also here I get back null as result.
blocTest('Should return valid option of user.',
build: () async {
when(MockIAuthFacade()
.getSignedInUser()
).thenAnswer((_) async => Future.value(optionOf<User>(tValidUser)));
return AuthBloc(MockIAuthFacade());
},
act: (bloc) async {
bloc.add(AuthEvent.authCheckRequested());
},
expect: [
]
);
}