I'm trying to code the Unit Test for my DataSourceImpl class, but I'm facing a problem with the methods that are called into the testing method.
I mocked and set the return for all methods into the DataSource testing method with the when statement, but it's not working...
Into the save and remove methods the Stream is called to notify the changes.
I'm using mocktail: ^0.1.4 for mocking dependency objects.
DATASOURCE IMPLEMENTATION
class DocumentDataSourceImpl implements IDocumentDataSource {
final FlutterSecureStorage _storage;
final StreamController<List<DocumentModel>> _streamController;
DocumentDataSourceImpl(this._storage, this._streamController);
#override
Future<void> remove(String id) async {
try {
await _storage.delete(key: id);
_streamController.sink.add(await getAll());
} catch (e) {
throw DocumentNotRemovedException();
}
}
#override
Future<void> save(DocumentModel model) async {
try {
await _storage.write(key: model.id, value: json.encode(model.toJson()));
_streamController.sink.add(await getAll());
} catch (e) {
throw DocumentNotSavedException();
}
}
#override
Future<List<DocumentModel>> getAll() async {
final result = await _storage.readAll();
return result.entries
.map((e) => DocumentModel.fromJson(json.decode(e.value)))
.toList();
}
}
UNIT TEST
void main() {
// Global Definitions
//-----Main Objects
late FlutterSecureStorageMock storage;
late StreamControllerMock<List<DocumentModel>> streamController;
late DocumentDataSourceImpl dataSource;
// SetUp Test
setUp(() {
//-----Objects Initialization
storage = FlutterSecureStorageMock();
streamController = StreamControllerMock();
dataSource = DocumentDataSourceImpl(storage, streamController);
});
group(("DocumentDataSource: Remove"), () {
final tArgument = "Document 1";
test("[P] Should remove the DocumentModel on Storage", () async {
//-----Arrange
when(() => storage.readAll())
.thenAnswer((_) async => tDocumentModelListMap);
when(() => dataSource.getAll())
.thenAnswer((_) async => tDocumentModelList);
when(() => streamController.sink.add(tDocumentModelList));
when(() => storage.delete(key: tArgument)).thenAnswer((_) async => null);
//-----Act
dataSource.remove(tArgument);
//-----Assert
verify(() => storage.delete(key: tArgument)).called(1);
verify(() => streamController.sink.add(tDocumentModelList)).called(1);
});
test(
"[N] Should return a DocumentNotRemovedException when the call of Storage is unsuccessful.",
() async {
//-----Arrange
when(() => storage.delete(key: tArgument)).thenThrow(
GenericExceptionMock(),
);
//-----Act
final result = dataSource.remove(tArgument);
//-----Assert
expect(result, throwsA(DocumentNotRemovedException()));
verify(() => storage.delete(key: tArgument)).called(1);
verifyNever(() => streamController.sink.add(tDocumentModelList));
});
});
group(("DocumentDataSource: Save"), () {
final tArgument = tDocumentModel;
test("[P] Should save the DocumentModel on Storage", () async {
//-----Arrange
when(() => storage.readAll())
.thenAnswer((_) async => tDocumentModelListMap);
when(() => dataSource.getAll())
.thenAnswer((_) async => tDocumentModelList);
when(() => streamController.sink.add(tDocumentModelList));
when(() => storage.write(
key: tArgument.id, value: json.encode(tArgument.toJson())));
//-----Act
dataSource.save(tArgument);
//-----Assert
verify(() => storage.write(
key: tArgument.id, value: json.encode(tArgument.toJson()))).called(1);
// verify(() => realTimeEvents.update(tDocumentModelList)).called(1);
});
test(
"[N] Should return a DocumentNotSavedException when the call of Storage is unsuccessful.",
() async {
//-----Arrange
when(() => storage.write(
key: tArgument.id, value: json.encode(tArgument.toJson()))).thenThrow(
GenericExceptionMock(),
);
//-----Act
final result = dataSource.save(tArgument);
//-----Assert
expect(result, throwsA(DocumentNotSavedException()));
verify(() => storage.write(
key: tArgument.id, value: json.encode(tArgument.toJson()))).called(1);
verifyNever(() => streamController.sink.add(tDocumentModelList));
});
});
}
MOCKS
class FlutterSecureStorageMock extends Mock implements FlutterSecureStorage {}
class StreamControllerMock<T> extends Mock implements StreamController<T> {}
ERROR LOG
type 'Null' is not a subtype of type 'Future<Map<String, String>>'
FlutterSecureStorageMock.readAll
DocumentDataSourceImpl.getAll
main.<fn>.<fn>.<fn>
when.<fn>
main.<fn>.<fn>
main.<fn>.<fn>
===== asynchronous gap ===========================
dart:async _completeOnAsyncError
package:memo/features/data/datasources/document_datasource_impl.dart DocumentDataSourceImpl.getAll
main.<fn>.<fn>.<fn>
when.<fn>
main.<fn>.<fn>
main.<fn>.<fn>
Bad state: Cannot call `when` within a stub response
when
main.<fn>.<fn>
main.<fn>.<fn>
✖ DocumentDataSource: Remove [P] Should remove the DocumentModel on Storage
Bad state: Cannot call `when` within a stub response
when
main.<fn>.<fn>
main.<fn>.<fn>
✖ DocumentDataSource: Remove [N] Should return a DocumentNotRemovedException when the call of Storage is unsuccessful.
Bad state: Cannot call `when` within a stub response
when
main.<fn>.<fn>
main.<fn>.<fn>
✖ DocumentDataSource: Save [P] Should save the DocumentModel on Storage
Bad state: Cannot call `when` within a stub response
when
main.<fn>.<fn>
main.<fn>.<fn>
✖ DocumentDataSource: Save [N] Should return a DocumentNotSavedException when the call of Storage is unsuccessful.
Exited (1)
Related
I'm using mockito for testing, riverpod for state management. I'm trying to test the method in my controller class but getting the FakeUsedError:
FakeUsedError: 'execute' No stub was found which matches the argument
of this method call: execute(Instance of 'AuthUseCaseInput').
I'm calling AuthUseCase class method from the AuthController class.
class AuthController extends StateNotifier<AuthState> {
final AuthUseCase authUseCase;
AuthController(this.authUseCase) : super(const AuthState.initial());
Future<void> mapAuthEventToAuthState(AuthEvent event) async {
state = const AuthState.loading();
await event.map(
signInWithEmailAndPassword: (signInWithEmailAndPassword) async {
final result = await authUseCase.execute(AuthUseCaseInput(
signInWithEmailAndPassword.email,
signInWithEmailAndPassword.password));
await result.fold(
(failure) async => state = AuthState.error(failure),
(login) async => state = const AuthState.loggedIn(),
);
});
}
The test class code is given below
void main() {
late AuthUseCase mockAuthUseCase;
late Login login;
late AuthUseCaseInput authUseCaseInput;
late AuthController authController;
setUpAll(() {
mockAuthUseCase = MockAuthUseCase();
login = LoginModel.fromJson(
json.decode(
jsonReader('helpers/dummy_data/login_success_response.json'),
),
).toEntity();
authUseCaseInput = AuthUseCaseInput(email, password);
when(mockAuthUseCase.execute(authUseCaseInput)).thenAnswer(
(_) async => Right(login),
);
authController = AuthController(mockAuthUseCase);
});
group('Auth Controller', () {
stateNotifierTest<AuthController, AuthState>(
'[AuthState.loggedIn] when sign in is success',
setUp: () async {
when(mockAuthUseCase.execute(authUseCaseInput))
.thenAnswer(
(_) async => Right(login),
);
},
actions: (notifier) => notifier.mapAuthEventToAuthState(
const SignInWithEmailAndPassword(email, password)),
expect: () => [const AuthState.loading(), const AuthState.loggedIn()],
build: () {
return authController;
});
});
}
i'm using mocktail package to mock my repositories for unit testing. I already tested out few of my controllers and its passing, until i encountered a error in my submitPhoneNumber method which I cant figure out where the problem is and what am i missing.
The setup is I'm stubbing my controller.submitPhoneNumber to answer a Future.value(true), which means success. so my expectations are the authRepository.registerWithPhoneNumber method should be called 1 time and the debugState.value.hasError should be false. But i'm getting this error:
No matching calls. All calls: MockFirebaseAuthRepository.registerWithPhoneNumber(+15551234567, Closure: (String) => void, Closure: () => void)
(If you called `verify(...).called(0);`, please instead use `verifyNever(...);`.)
package:test_api fail
package:mocktail/src/mocktail.dart 722:7 _VerifyCall._checkWith
package:mocktail/src/mocktail.dart 515:18 _makeVerify.<fn>
test/src/features/authentication/presentation/sign_in/phone_sign_in/sign_in_controller_test.dart 41:13 main.<fn>.<fn>
this means that the controller.submitPhoneNumber has not been called and the debugState.value.hasError is true. But in actual, my controller is working fine in both emulator and physical device.
I cant figure out why this is happening so maybe I made mistake on how I stubbed my function.
Appreciated so much if someone could help me figure this out.
Here is my test:
void main() {
group('controller submit phone number test', () {
final controller = SignInController(
formType: SignInFormType.register, authRepository: authRepository);
test('submit phonumber success', () async {
const phoneNumber = '+15551234567';
///this is where the error is comming from
when(() => controller.submitPhoneNumber(phoneNumber, (e) {}, () {}))
.thenAnswer((_) => Future.value(true));
await controller.submitPhoneNumber(phoneNumber, (e) {}, () {});
verify(() => authRepository.registerWithPhoneNumber(
phoneNumber, (e) {}, () {})).called(1);
expect(controller.debugState.value.hasError, false);
expect(controller.debugState.formType, SignInFormType.register);
expect(controller.debugState.value, isA<AsyncValue>());
}, timeout: const Timeout(Duration(milliseconds: 500)));
}
And here is my controller
class SignInController extends StateNotifier<SignInState> {
SignInController({
required SignInFormType formType,
required this.authRepository,
}) : super(SignInState(formType: formType));
final AuthRepository authRepository;
Future<bool> submitPhoneNumber(String phoneNumber,
void Function(String e) verificationFailed, VoidCallback codeSent) async {
state = state.copyWith(value: const AsyncValue.loading());
final value =
await AsyncValue.guard(() => authRepository.registerWithPhoneNumber(
phoneNumber,
verificationFailed,
codeSent,
));
state = state.copyWith(value: value);
return value.hasError == false;
}
}
final signInControllerProvider = StateNotifierProvider.autoDispose
.family<SignInController, SignInState, SignInFormType>((ref, formType) {
final authRepository = ref.watch(authRepositoryProvider);
return SignInController(
authRepository: authRepository,
formType: formType,
);
});
The callbacks you are passing to the controller during setup, invocation, and verification are not the same instances.
There are two ways you can fix this:
Before your setup, create local variables for verificationFailed and codeSent. Pass these instead of an anonymous function.
Use any<T> where T is the type of your parameter (void Function(String) and VoidCallback).
void errorFunction(String value) {
value;
}
void onSentCode() {}
when(() => controller.submitPhoneNumber(
phoneNumber, any<void Function(String)>(), any<VoidCallback>()))
.thenAnswer((_) => Future.value(true));
await controller.submitPhoneNumber(
phoneNumber, errorFunction, onSentCode);
verify(() => controller.submitPhoneNumber(
phoneNumber, errorFunction, onSentCode)).called(1);
I already implementation of Drift for local storage, and want make it testable function. But I get stack and idk how to fix it the unit test.
HomeDao
#DriftAccessor(tables: [RepositoriesTable])
class HomeDao extends DatabaseAccessor<AppDatabase> with _$HomeDaoMixin {
HomeDao(AppDatabase db) : super(db);
Future<List<RepositoriesTableData>> getRepositories() async =>
await select(repositoriesTable).get();
}
AppDatabase
#DriftDatabase(
tables: [RepositoriesTable],
daos: [HomeDao],
)
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
#override
int get schemaVersion => 1;
}
QueryExecutor _openConnection() {
return SqfliteQueryExecutor.inDatabaseFolder(
path: 'db.sqlite',
logStatements: true,
);
}
LocalDataSources
abstract class GTHomeLocalDataSource {
const GTHomeLocalDataSource();
Future<List<RepositoriesTableData>> getRepositories();
}
class GTHomeLocalDataSourceImpl implements GTHomeLocalDataSource {
final AppDatabase appDatabase;
const GTHomeLocalDataSourceImpl({required this.appDatabase});
#override
Future<List<RepositoriesTableData>> getRepositories() async =>
await appDatabase.homeDao.getRepositories();
}
UnitTesting
void main() => testGTHomeLocalDataSource();
class MockDatabaseHandler extends Mock implements AppDatabase {}
void testGTHomeLocalDataSource() {
late GTHomeLocalDataSource localDataSource;
late AppDatabase databaseHandler;
setUp(() {
databaseHandler = MockDatabaseHandler();
localDataSource = GTHomeLocalDataSourceImpl(
appDatabase: databaseHandler,
);
});
group("GTHomeLocalDataSource -", () {
test(''' \t
GIVEN Nothing
WHEN call getRepositories
THEN databaseHandler select function has been called and return list of RepositoriesTableData
''', () async {
// GIVEN
when(() => databaseHandler.homeDao.getRepositories())
.thenAnswer((_) => Future.value(repositoriesDummyTable));
// WHEN
final result = await localDataSource.getRepositories();
// THEN
verify(() => databaseHandler.homeDao.getRepositories());
expect(result, isA<List<RepositoriesTableData>>());
expect(result.length, repositoriesDummyTable.length);
expect(result.first.language, repositoriesDummyTable.first.language);
});
});
tearDown(() async {
await databaseHandler.close();
});
}
My function is work well for get data from the local db and show it in the app, but when running as unit test, I stacked with this error.
package:gt_core/local/database/database_module.g.dart 424:22 MockDatabaseHandler.homeDao
package:gt_home/data/data_sources/gt_home_local_datasource.dart 20:25 GTHomeLocalDataSourceImpl.getRepositories
test/data/data_sources/gt_home_local_datasource_test.dart 35:44 testGTHomeLocalDataSource.<fn>.<fn>
test/data/data_sources/gt_home_local_datasource_test.dart 29:12 testGTHomeLocalDataSource.<fn>.<fn>
type 'Null' is not a subtype of type 'Future<void>'
package:drift/src/runtime/api/db_base.dart 125:16 MockDatabaseHandler.close
test/data/data_sources/gt_home_local_datasource_test.dart 47:27 testGTHomeLocalDataSource.<fn>
test/data/data_sources/gt_home_local_datasource_test.dart 46:12 testGTHomeLocalDataSource.<fn>
===== asynchronous gap ===========================
dart:async _completeOnAsyncError
test/data/data_sources/gt_home_local_datasource_test.dart testGTHomeLocalDataSource.<fn>
test/data/data_sources/gt_home_local_datasource_test.dart 46:12 testGTHomeLocalDataSource.<fn>
type 'Future<List<RepositoriesTableData>>' is not a subtype of type 'HomeDao'
Anyone know how to fix it?
If you use Mocking to test Drift database, then you'll need to mock the method call as well, otherwiser the method will return null which is the default behavior for Mockito. For example.
// return rowid
when(db.insertItem(any)).thenAnswer((_) => 1);
However it is recommended as per Drift documentation to use in-memory sqlite database which doesn't require real device or simulator for testing.
This issue was also has been discussed here
Using in memory database
import 'package:drift/native.dart';
import 'package:test/test.dart';
import 'package:my_app/src/database.dart';
void main() {
MyDatabase database;
MyRepo repo;
setUp(() {
database = MyDatabase(NativeDatabase.memory());
repo = MyRepo(appDatabase: database);
});
tearDown(() async {
await database.close();
});
group('mytest', () {
test('test create', () async {
await repo.create(MyDateCompanion(title: 'some name'));
final list = await repo.getItemList();
expect(list, isA<MyDataObject>())
})
});
}
I am trying to create a simple test but I keep getting this error.
type 'Null' is not a subtype of type 'Future'
test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:async/async.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
class MockClient extends Mock implements http.Client {}
void main() {
group('signin', () {
final client = MockClient();
final api = AuthApi('https://baseUrl', client);
final credential = Credential(
email: 'test#test.com',
type: AuthType.email,
password: 'pass',
);
test('should return error when status code is not 200', () async {
registerFallbackValue(Uri.parse(''));
when(() => client.post(any(), body: {}))
.thenAnswer((_) async => http.Response('{}', 404));
final result = await api.signIn(credential);
expect(result, isA<ErrorResult>());
});
});
}
Error is at line
final result = await api.signIn(credential); expect(result,
isA());
If I remove those lines I don't see the error.
auth_api.dart
class AuthApi implements IAuthApi {
AuthApi(this.baseUrl, this._client);
final http.Client _client;
String baseUrl;
#override
Future<Result<String>> signIn(Credential credential) async {
final endpoint = Uri.parse(baseUrl + '/auth/signin');
return await _postCredential(endpoint, credential);
}
#override
Future<Result<String>> signUp(Credential credential) async {
final endpoint = Uri.parse(baseUrl + '/auth/signup');
return await _postCredential(endpoint, credential);
}
Future<Result<String>> _postCredential(
Uri endpoint,
Credential credential,
) async {
final response =
await _client.post(endpoint, body: Mapper.toJson(credential));
if (response.statusCode != 200) {
return Result.error('Server Error');
}
var json = jsonDecode(response.body);
return json['auth_token'] != null
? Result.value(json['auth_token'])
: Result.error(json['message']);
}
}
I checked other similar question answers also but none of them worked. I am using mocktail package & http for post.
The problem is in that line:
when(() => client.post(any(), body: {}))
.thenAnswer((_) async => http.Response('{}', 404));
It means that when there's a client.post() method invoked with any() URL and a specific empty body {}, then it should return a mocked response.
What you want is to return a mocked response when there's any URL and any body, so it should be like this:
when(() => client.post(any(), body: any(named: 'body')))
.thenAnswer((_) async => http.Response('{}', 404));
However, if you want to test if a specific error is thrown, that code should be modified:
test('should return error when status code is not 200', () async {
when(() => client.post(any(), body: any(named: 'body')))
.thenThrow(ErrorResult(Exception()));
expect(() async => await api.signIn(credential),
throwsA(isA<ErrorResult>()));
});
First, you specify that calling API should throw an error (when(...).thenThrow(...)) and then you check if an error was thrown (expect(..., throwsA(...)))
i am trying to mock the following method of sqlite_api.dart by (https://pub.dev/packages/sqflite):
Future<T> transaction<T>(Future<T> Function(Transaction txn) action, {bool? exclusive});
my implementation/adapting of the method is like:
Future<void> _transaction(Set<DatabaseLocalRequest> payload) async {
await this._api.transaction((txn) async => {
for (final req in payload) {
await txn.rawInsert(req.query.sql, req.query.arguments)
}
});
}
my db_test.dart using Mocktail (https://pub.dev/packages/mocktail):
test('if [single] put succeeds', () async {
// SETUP
sut = DatabaseLocalProvider(db: mockDb);
final query = Statement(sql: 'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
final req = DatabaseLocalRequest(query: query);
// MOCK
when(() => mockDb.transaction((txn) => txn.rawInsert(req.query.sql, req.query.arguments)))
.thenAnswer((_) async => 1);
// ACT, ASSERT
await sut.put(req: req, bulkReq: null).then((response) => {
expect(response, ...
});
}); // test end
I got the following response from the console ERROR:
🚨🚨
type 'Null' is not a subtype of type 'Future<Set<Set<int>>>'
How do I stub the inner txn.rawInsert() method that should respond with the Future<Set<Set<int>>> with {{1}}?
Thanks in advance!
I might not respond exactly to your question but you can mock sqflite by using a real implementation with sqflite_common_ffi since it works on all desktop (MacOS, Linux, Windows) on the dart VM so also in flutter and dart unit tests:
More information here: https://pub.dev/packages/sqflite_common_ffi#unit-test-code
One solution is open a database in memory for each test so that you start with an empty database.
import 'package:test/test.dart';
import 'package:sqflite_common/sqlite_api.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
void main() {
// Init ffi loader if needed.
sqfliteFfiInit();
test('simple sqflite example', () async {
var db = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath);
expect(await db.getVersion(), 0);
await db.close();
});
}
when(() => mockDb.transaction(any())).thenAnswer((_) async => {{1}});
when(() => mockDb.rawInsert(any())).thenAnswer((_) async => 1);
this did the trick! but it is not 100 solution, because the closure is not stubbed but bypassed.