I'm trying to test my whole AuthManager class which use FirebaseAuth to signin, login. Here my file:
class AuthManager extends ChangeNotifier {
final FirebaseAuth auth;
Stream<User?> get user => auth.authStateChanges();
static Future<FirebaseApp> initializeFirebase({
required BuildContext context,
}) async {
FirebaseApp firebaseApp = await Firebase.initializeApp();
return firebaseApp;
}
AuthManager({required this.auth});
Future<String> signup(String email, String password) async {
try {
final credential = await auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
return "Success";
} on FirebaseAuthException catch (e) {
rethrow;
}
}
Future<String> signInWithEmailAndPassword(
String email, String password) async {
try {
final userCredential = await auth.signInWithEmailAndPassword(
email: email, password: password);
return "Success";
} on FirebaseAuthException catch (e) {
return "Failed";
} catch (e) {
rethrow;
}
}
static Future<String> signOut() async {
try {
await FirebaseAuth.instance.signOut();
return "Success";
} catch (e) {
rethrow;
}
}
}
I used to return the usercredential but wanted to try test a simple string return for the test, following this tutorial: https://www.youtube.com/watch?v=4d6hEaUVvuU, here is my test file
import 'package:firebase_auth/firebase_auth.dart';
import 'package:notes_firebase_app/data/models/auth_manager.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
class MockFirebaseAuth extends Mock implements FirebaseAuth {
#override
Stream<User> authStateChanges() {
return Stream.fromIterable([
_mockUser,
]);
}
}
class MockUser extends Mock implements User {}
final MockUser _mockUser = MockUser();
class MockUserCredential extends Mock implements Future<UserCredential> {}
void main() {
final MockFirebaseAuth mockFirebaseAuth = MockFirebaseAuth();
final AuthManager authManager = AuthManager(auth: mockFirebaseAuth);
final MockUserCredential mockUserCredential = MockUserCredential();
setUp(() {});
test("emit occurs", () async {
expectLater(authManager.user, emitsInOrder([_mockUser]));
});
test("create account", () async {
when(mockFirebaseAuth.createUserWithEmailAndPassword(
email: "tadas#gmail.com", password: "123456"))
.thenAnswer((realInvocation) => null);
expect(
await authManager.signInWithEmailAndPassword(
"tadas#gmail.com", "123456"),
"Success");
});
}
I face two problems here, cannot pass null because we need to handle it now or this throw this error
The return type 'Null' isn't a 'Future<UserCredential>', as required by the closure's context
Then I tried to mock UserCredential like this.
final MockUserCredential mockUserCredential = MockUserCredential();
when(mockFirebaseAuth.createUserWithEmailAndPassword(
email: "tadas#gmail.com", password: "123456"))
.thenAnswer((realInvocation) => mockUserCredential);
but I'm getting this error:
type 'Null' is not a subtype of type 'Future<UserCredential>'
What am I doing wrong ? Help will be much appreciated.
I am not totally sure but mockito package may need a generator. Try mocktail package and use
when(()=> mockFirebaseAuth.createUserWithEmailAndPassword(
email: "tadas#gmail.com", password: "123456")).thenAnswer((realInvocation) => null);
use callback function in when().
Related
I am nit sure about this error because user should be inithialized in Auth Provider and then I will be able to use it in User Provider but flutter continue giving this error.
Here is my code. Can someone help to solve or tell me a better form to organize it?
AuthProvider
class AuthProvider extends ChangeNotifier {
late final FirebaseAuth _auth;
late final NavigationService _navigationService;
late final DatabaseService _databaseService;
late UserData user;
AuthProvider() {
_auth = FirebaseAuth.instance;
_navigationService = GetIt.instance.get<NavigationService>();
_databaseService = GetIt.instance<DatabaseService>();
_auth.authStateChanges().listen((_user) {
if (_user != null) {
//_databaseService.updateUserLastSeenTime(_user.uid);
_databaseService.getUser(_user.uid).then(
(_snapshot) {
if (_snapshot.exists) {
if (_snapshot.data() != null) {
user =
UserData.fromJson(jsonDecode(jsonEncode(_snapshot.data())));
notifyListeners();
}
}
_navigationService.removeAndNavigateToRoute('/home');
},
);
} else {
_navigationService.removeAndNavigateToRoute('/login');
}
});
}
User Provider
class UserProvider with ChangeNotifier {
final DatabaseService _databaseService = DatabaseService();
UserData _user = AuthProvider().user;
UserData get getUser => _user;
Future<void> refreshUser() async {
UserData user = await _databaseService.getUserDetails();
_user = user;
notifyListeners();
}
// update user name
Future<void> editName(String name) async {
try {
await _databaseService.getUserDoc(_user.uid).update({'name': name});
} catch (err) {
print(err.toString());
}
}
// update user last name
Future<void> editLastName(String lastName) async {
try {
await _databaseService
.getUserDoc(_user.uid)
.update({'lastName': lastName});
} catch (err) {
print(err.toString());
}
}
}
I'm trying to write the code of the ApplicationState in Get to know Firebase for Flutter codelab while practicing Test Driven Development.
The method signOut in the codelab should be like this:
void signOut() {
// The question is about how to test the effect of this invocation
FirebaseAuth.instance.signOut();
}
If I understand it right, FirebaseAuth.instance.signOut() should make FirebaseAuth.instance.userChanges() invoke its listener with Stream<User?> that contains a null User. So the effect of invoking FirebaseAuth.instance.signOut() is not direct. I don't know how to mock this. According to Test Driven Development, I should never write a code unless I do that to let (a failing test) pass.
The problem is how to write a failing test that forces me to write the following code:
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loginState = ApplicationLoginState.loggedIn;
}
////////////////////////////////////////////////////////////
else {
_loginState = ApplicationLoginState.loggedOut; ////
}
////////////////////////////////////////////////////////////
notifyListeners();
});
}
I can mock FirebaseAuth.instance.signOut() like this:
// firebaseAuth is a mocked object
when(firebaseAuth.signOut())
.thenAnswer((realInvocation) => Completer<void>().future);
// sut (system under test)
sut.signOut();
verify(firebaseAuth.signOut()).called(1);
And this forces me to invoke FirebaseAuth.instance.signOut(). But this doesn't force me to write the aforementioned code to let it pass.
I test this code:
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
FirebaseAuth.instance.userChanges().listen((user) {
/////////////////////////////////////////////////////////////
if (user != null) {
_loginState = ApplicationLoginState.loggedIn; ////
}
/////////////////////////////////////////////////////////////
else {
_loginState = ApplicationLoginState.loggedOut;
}
notifyListeners();
});
}
By mocking FirebaseAuth.instance.signInWithEmailAndPassword():
final userCredential = MockUserCredential();
// firebaseAuth is a mocked object
when(firebaseAuth.signInWithEmailAndPassword(
email: validEmail, password: password))
.thenAnswer((realInvocation) => Future.value(userCredential));
// sut (system under test)
await sut.signInWithEmailAndPassword(
validEmail, password, firebaseAuthExceptionCallback);
// This is the direct effect on my class, that will happen by the aforementioned code
expect(sut.loginState, ApplicationLoginState.loggedIn);
Please, be patient with me. I'm new to doing testing and Test Driven Development.
Here is what I did.
Instead of calling FirebaseAuth.instance.userChanges().listen() once in the init() method as in the codelab, I called it twice, one time on the signInWithEmailAndPassword() method, and one time on the signOut() method.
Future<void> signInWithEmailAndPassword(String email, String password,
void Function(FirebaseAuthException exception) errorCallback) async {
///////////////////////////////////////////////////////////////////
firebaseAuth.userChanges().listen(_whenNotNullUser); ////
///////////////////////////////////////////////////////////////////
try {
await firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
} on FirebaseAuthException catch (exception) {
errorCallback(exception);
}
}
Future<void> signOut() async {
///////////////////////////////////////////////////////////////////
firebaseAuth.userChanges().listen(_whenNullUser); ////
///////////////////////////////////////////////////////////////////
await firebaseAuth.signOut();
}
void _whenNullUser(User? user) {
if (user == null) {
_loginState = ApplicationLoginState.loggedOut;
notifyListeners();
}
}
void _whenNotNullUser(User? user) {
if (user != null) {
_loginState = ApplicationLoginState.loggedIn;
notifyListeners();
}
}
And this is my test:
test("""
$given $workingWithApplicationState
$wheN Calling signInWithEmailAndPassword()
$and Calling loginState returns ApplicationLoginState.loggedIn
$and Calling signOut()
$and Calling loginState returns ApplicationLoginState.loggedOut
$and Calling signInWithEmailAndPassword()
$then Calling loginState should return ApplicationLogginState.loggedIn
$and $notifyListenersCalled
""", () async {
when(firebaseAuth.signInWithEmailAndPassword(
email: validEmail, password: password))
.thenAnswer((realInvocation) => Future.value(userCredential));
await sut.signInWithEmailAndPassword(
validEmail, password, firebaseAuthExceptionCallback);
expect(sut.loginState, ApplicationLoginState.loggedIn);
reset(notifyListenerCall);
prepareUserChangesForTest(nullUser);
await sut.signOut();
verify(firebaseAuth.signOut()).called(1);
expect(sut.loginState, ApplicationLoginState.loggedOut);
reset(notifyListenerCall);
prepareUserChangesForTest(notNullUser);
when(firebaseAuth.signInWithEmailAndPassword(
email: validEmail, password: password))
.thenAnswer((realInvocation) => Future.value(userCredential));
await sut.signInWithEmailAndPassword(
validEmail, password, firebaseAuthExceptionCallback);
expect(sut.loginState, ApplicationLoginState.loggedIn);
verify(notifyListenerCall()).called(1);
});
Now I'm forced to write the login in both _whenNullUser() and _whenNotNullUser() methods to pass my test.
I'm brand new to flutter and I want to try to make a Unit Test for one functions that I've created. The function simply test if the email & password that the user provides to login are correct or not. So, the function provides a connection to the database and verify if the email & password are valid.
I tried to make a Unit Test with mockito to emulate the Firebase but it don't work very well. My function return an "UserCredential" type and I don't know how to verify it with Mocks. I tried to create a Mock for this type, but it says that "type 'Null' is not a subtype of type 'Future'"...
Anyone can make a clear explanation ? :)
Firebase Function to test
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flexmes/data/models/user_model.dart';
class UserAPI{
final CustomUser _customUser = CustomUser();
final FirebaseAuth auth;
UserAPI({required this.auth});
Future<UserCredential?> signInWithEmailAndPassword(String email, String password) async {
try{
UserCredential result = await auth.signInWithEmailAndPassword(email: email, password: password);
print (result);
return result;
} catch (error){
print (error);
return null;
}
}
}
Unit Test for the signInWithEmailAndPassword function
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flexmes/data/data_providers/user_provider.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
class MockFirebaseAuth extends Mock implements FirebaseAuth{}
class MockUserCredential extends Mock implements UserCredential{}
void main(){
final MockFirebaseAuth mockAuth = MockFirebaseAuth();
final MockUserCredential userCredential = MockUserCredential();
group('description', () {
late UserAPI userAPI;
setUp(() {
userAPI = UserAPI(auth: mockAuth);
});
tearDown(() {
});
test('Sign in with Email & Password', () async {
when(mockAuth.signInWithEmailAndPassword(email: "admin#admin.com", password: "password")).
thenAnswer((_) => Future<MockUserCredential>.value(userCredential));
expect(await userAPI.signInWithEmailAndPassword("admin#admin.com", "password"), userCredential);
});
});
}
There is mock authentication package available firebase_auth_mocks and google_sign_in_mocks
You can replace your code like this
group('description', () {
final _mockAuth = MockFirebaseAuth(mockUser: _mockUser);
late UserAPI userAPI;
setUp(() {
userAPI = UserAPI(auth: _mockAuth);
});
tearDown(() {});
const _email = 'ilyas#yopmail.com';
const _uid = 'sampleUid';
const _displayName = 'ilyas';
const _password = 'Test#123';
final _mockUser = MockUser(
uid: _uid,
email: _email,
displayName: _displayName,
);
test('signIn function test', () async {
final user = await userAPI.signInWithEmailAndPassword(_email, _password);
expect(user?.uid, _mockUser.uid);
});
});
google_sign_in_mocks this you can use for GoogleSignIn test
I get the below error when I run the code, Pls help me
error: The argument type 'User' can't be assigned to the parameter type 'User1'. (argument_type_not_assignable at [time_tracker_app] lib\services\auth.dart:34)
here is my code :
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
class User1 {
User1({#required this.uid, });
final String uid;
}
abstract class AuthBase {
Future<User1> currentUser();
Future<User1> signInAnonymously();
Future<void> signOut();
}
class Auth implements AuthBase {
final _firebaseAuth = FirebaseAuth.instance;
User1 _userFromFirebase(User1 user) {
if (user == null) {
return null;
}
return User1(uid: user.uid);
}
#override
Future<User1> currentUser() async {
final user = _firebaseAuth.currentUser;
return _userFromFirebase(User1(uid: user.uid));
}
#override
Future<User1> signInAnonymously() async {
final authResult = await _firebaseAuth.signInAnonymously();
return _userFromFirebase(authResult.user);
}
Future<void> signOut() async {
await _firebaseAuth.signOut();
}
}
The issue is in the _userFromFirebase function parameter. From the signInAnonymously function you are calling the _userFromFirebase function with Firebase User object.
#override
Future<User1> signInAnonymously() async {
final authResult = await _firebaseAuth.signInAnonymously();
return _userFromFirebase(authResult.user); // Passing User object
}
To fix the issue, you need to change the parameter type:
User1 _userFromFirebase(User user) {
if (user == null) {
return null;
}
return User1(uid: user.uid);
}
I have the following repository and I'd like to test it. I know this may be a silly question but I'm still learning.
class AuthRepository implements AuthBaseRepository {
final Reader _read;
const AuthRepository(this._read);
#override
Future<User> login({String email, String password}) async {
try {
final response = await _read(dioProvider).post(
'/sign_in',
data: {
"user": {
"email": email,
"password": password,
},
},
);
return _mapUserFromResponse(response);
} on DioError catch (_) {
throw const CustomException(message: 'Invalid login credentials.');
} on SocketException catch (_) {
const message = 'Please check your connection.';
throw const CustomException(message: message);
}
}
And this is what I've done so far:
void main() {
test('loadUser', () async {
Dio dio;
DioAdapterMockito dioAdapterMockito;
AuthRepository repository;
setUpAll(() {
dio = Dio();
dioAdapterMockito = DioAdapterMockito();
dio.httpClientAdapter = dioAdapterMockito;
repository = AuthRepository(_reader_here_);
});
test('mocks any request/response via fetch method', () async {
final responsePayload =
await parseJsonFromAssets("assets/api-response.json");
final responseBody = ResponseBody.fromString(
responsePayload,
200,
headers: {
Headers.contentTypeHeader: [Headers.jsonContentType],
},
);
when(dioAdapterMockito.fetch(any, any, any))
.thenAnswer((_) async => responseBody);
});
});
}
I have no idea of how to mock Reader. Basically, I've seen something like class MyMock extends Mock implements Something but Reader is not a class, it's a function so I'm completely lost.
Any help/tips/examples will be appreciated.
Thanks in advance!
Instead of trying to mock a Reader, create a provider for your repository and use ProviderContainer to read it.
class AuthRepository implements AuthBaseRepository {
const AuthRepository(this._read);
static final provider = Provider<AuthRepository>((ref) => AuthRepository(ref.read));
final Reader _read;
#override
Future<User> login({String email, String password}) async {
...
}
Example usage:
final user = createTestUser();
final container = ProviderContainer(
overrides: [
// Example of how you can mock providers
dio.overrideWithProvider(mockDio),
],
);
final repo = container.read(AuthRepository.provider);
expectLater(
await repo.login(email: 'AzureDiamond', password: 'hunter2'),
user,
);
You could also consider using the overrides in ProviderContainer to mock Dio instead of involving a mocking framework to simplify your tests further.
More on testing here.