I have made a simple unit test which tests a coroutines function which uses firebase.
I've mocked all the dependencies and the methods being used in this function I'm testing, but it continues to hang. I'm not sure exactly where it's hanging, but I would assume on the mocking of firestore, and it has await().
Test Class:
import android.content.Context
import com.example.socialtoker.data.db.UserDao
import com.example.socialtoker.data.repository.UserDataRepository
import com.example.socialtoker.data.repository.UserDataRepositoryImpl
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.DatabaseReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.storage.FirebaseStorage
import io.mockk.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
#RunWith(JUnit4::class)
#ExperimentalCoroutinesApi
class UserDataRepositoryImplTest {
private val contextMock = mockk<Context>{
every { getExternalFilesDir(any())?.absolutePath } returns "src/SocialToker/"
}
private val firestoreMock = mockk<FirebaseFirestore>{
coEvery { collection("Users").document(any()).set(any()).await() } returns mockk()
}
private val firebaseAuthMock = mockk<FirebaseAuth>{
coEvery { createUserWithEmailAndPassword(any(), any()) } returns mockk()
every { currentUser?.uid } returns "UID"
}
private val firebaseStorageMock = mockk<FirebaseStorage>()
private val firebaseDatabaseMock = mockk<DatabaseReference>()
private val daoMock = mockk<UserDao>{
coEvery { addUser(any()) } returns mockk()
}
private lateinit var userDateRepository: UserDataRepository
private val emailAddress = "socialtoker#socialtoker.com"
private val password = "socialtokerpassword"
private val username = "socialtoker"
#Before
fun setup() {
userDateRepository = UserDataRepositoryImpl(
contextMock,
firestoreMock,
firebaseAuthMock,
firebaseStorageMock,
firebaseDatabaseMock,
daoMock
)
}
#Test
fun `createUser calls firebase and stores user info locally and remotely`() = runBlocking {
// WHEN
userDateRepository.createUser(emailAddress, password, username)
//THEN
coVerify { firebaseAuthMock.createUserWithEmailAndPassword(emailAddress, password) }
}
}
Test Subject:
override suspend fun createUser(email: String, password: String, username: String): AuthResult {
try {
val data = hashMapOf(
"name" to username
)
val authResult = firebaseAuth.createUserWithEmailAndPassword(email, password).await()
val uid = firebaseAuth.currentUser!!.uid
userDao.addUser(UserData(uid, username, "", ""))
firestoreRef.collection("Users")
.document(uid)
.set(data).await()
return authResult
} catch (error: Throwable) {
throw RepositoryError(
error.localizedMessage ?: "Unable to create user", error
)
}
}
Please note that await is an extension function on Task class.
Therefore Mocking extension functions might need to be taken into consideration.
Related
I hope you are all doing well.
I have run into a few problems and can't seem to find the answer online in an applicable way to my scenario. I am coding in Kotlin.
My database resides on an external host but I have already got the database connection set up and connected.
I have a login activity with a username and password field, and a login button. I have managed to verify the login details by setting specific credentials but I now have a database, SQL, linked to my android app. How do I verify the login credentials the user input against the database and check if the user is active?
Name: Bobby
Database Host: sql99.dbn7.host-h.net
Database Name: JimmysWorldDB
Driver Source: Built-in library
The tables used here are as follows:
1. UserLogins
Column 1 : UserID
Column 2 : FullName
Column 3 : Username
Column 4 : Password
Column 5 : HasAccess
2. LoginRecords
Column 1 : RecordID
Column 2 : Date
Column 3 : Logon <--- This is a time field
Column 4 : Logoff <--- This is a time field
Column 5 : Username
So basically I would like to know how to make the app check the verify the Username and Password and only if the member HasAccess = true then have a successful login. <--- All from UserLogins table
Then if the user has logged in successfully, save a LoginRecord where it puts the date, login time and the username.
My code is as follows below.
LoginActivity.kt
name of the button is button_login
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.jimmysworld.MainActivity
import com.jimmysworld.R
import com.jimmysworld.databinding.ActivityLoginBinding
class LoginActivity : AppCompatActivity() {
private lateinit var loginViewModel: LoginViewModel
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
val username = binding.username
val password = binding.password
val login = binding.login
val loading = binding.loading
loginViewModel = ViewModelProvider(this, LoginViewModelFactory())[LoginViewModel::class.java]
loginViewModel.loginFormState.observe(this#LoginActivity, Observer {
val loginState = it ?: return#Observer
// disable login button unless both username / password is valid
login.isEnabled = loginState.isDataValid
if (loginState.usernameError != null) {
username.error = getString(loginState.usernameError)
}
if (loginState.passwordError != null) {
password.error = getString(loginState.passwordError)
}
})
loginViewModel.loginResult.observe(this#LoginActivity, Observer {
val loginResult = it ?: return#Observer
loading.visibility = View.GONE
if (loginResult.error != null) {
showLoginFailed(loginResult.error)
}
if (loginResult.success != null) {
updateUiWithUser(loginResult.success)
}
setResult(Activity.RESULT_OK)
//Complete and destroy login activity once successful
finish()
})
username.afterTextChanged {
loginViewModel.loginDataChanged(
username.text.toString(),
password.text.toString()
)
}
password.apply {
afterTextChanged {
loginViewModel.loginDataChanged(
username.text.toString(),
password.text.toString()
)
}
setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE ->
loginViewModel.login(
username.text.toString(),
password.text.toString()
)
}
false
}
login.setOnClickListener {
loading.visibility = View.VISIBLE
loginViewModel.login(username.text.toString(), password.text.toString())
}
}
}
private fun updateUiWithUser(model: LoggedInUserView) {
val welcome = getString(R.string.welcome)
val displayName = model.displayName
// TODO : initiate successful logged in experience
Toast.makeText(
applicationContext,
"$welcome $displayName",
Toast.LENGTH_LONG
).show()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
private fun showLoginFailed(#StringRes errorString: Int) {
Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show()
}
}
LoginViewModel.kt
import android.util.Patterns
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.jimmysworld.R
import com.jimmysworld.data.LoginRepository
import com.jimmysworld.data.Result
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
private val _loginForm = MutableLiveData<LoginFormState>()
val loginFormState: LiveData<LoginFormState> = _loginForm
private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _loginResult
fun login(username: String, password: String) {
// can be launched in a separate asynchronous job
val result = loginRepository.login(username, password)
val user = "Admin"
val pass = "1234567"
if (username.toString() == user && password.toString() == pass) {
if (result is Result.Success) {
_loginResult.value =
LoginResult(success = LoggedInUserView(displayName = result.data.displayName))
}
} else {
_loginResult.value = LoginResult(error = R.string.login_failed)
}
}
fun loginDataChanged(username: String, password: String) {
if (!isUserNameValid(username)) {
_loginForm.value = LoginFormState(usernameError = R.string.invalid_username)
} else if (!isPasswordValid(password)) {
_loginForm.value = LoginFormState(passwordError = R.string.invalid_password)
} else {
_loginForm.value = LoginFormState(isDataValid = true)
}
}
// A placeholder username validation check
private fun isUserNameValid(username: String): Boolean {
return if (username.contains('#')) {
Patterns.EMAIL_ADDRESS.matcher(username).matches()
} else {
username.isNotBlank()
}
}
// A placeholder password validation check
private fun isPasswordValid(password: String): Boolean {
return password.length > 7
}
}
LoggedInUser
import com.jimmysworld.data.model.LoggedInUser
class LoginRepository(val dataSource: LoginDataSource) {
var user: LoggedInUser? = null
private set
val isLoggedIn: Boolean
get() = user != null
init {
user = null
}
fun logout() {
user = null
dataSource.logout()
}
fun login(username: String, password: String): Result<LoggedInUser> {
// handle login
val result = dataSource.login(username, password)
if (result is Result.Success) {
setLoggedInUser(result.data)
}
return result
}
private fun setLoggedInUser(loggedInUser: LoggedInUser) {
this.user = loggedInUser
}
}
LoginViewModelFactory.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.jimmysworld.data.LoginDataSource
import com.jimmysworld.data.LoginRepository
class LoginViewModelFactory : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
return LoginViewModel(
loginRepository = LoginRepository(
dataSource = LoginDataSource()
)
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
I am sorry for it being so long winded and appreciate any help I can get.
Thanks in advance.
You probably need a database driver.
I have not written for Android, but it looks like Android does not natively support mysql db connections. One way to make this happen is to use a PHP middleman. That's wild.
Here is a link about using PHP for this purpose. https://www.tutorialspoint.com/android/android_php_mysql.htm
I am getting this exception while unit testing the viewModel.
Exception in thread "UI thread #coroutine#1" java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.paging.CachedPagingDataKt.cachedIn, parameter <this>
at androidx.paging.CachedPagingDataKt.cachedIn(CachedPagingData.kt)
at com.sarmad.newsprism.news.ui.NewsViewModel$getNewsStream$1.invokeSuspend(NewsViewModel.kt:46)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [CoroutineId(1), "coroutine#1":StandaloneCoroutine{Cancelling}#67526363, Dispatchers.Main]
expected:<false> but was:<true>
Expected :false
Actual :true
I want to test if newsViewModel.getNewsStream() is called, it should start loading, stop loading and expose updated UiState to NewsFragment so fragment can call adapter.submitData(data). But, there is an exception indication an error (I am a beginner and I can't understand that even after research for a good amount of time) in cachedIn(viewModelScope) operator while I am collecting flow in viewModel.
NewsViewModel
package com.sarmad.newsprism.news.ui
#HiltViewModel
class NewsViewModel #Inject constructor(
private val newsRepository: NewsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
companion object {
const val KEY_SUBREDDIT = "us"
const val DEFAULT_SUBREDDIT = "androiddev"
}
init {
if (!savedStateHandle.contains(KEY_SUBREDDIT)) {
savedStateHandle[KEY_SUBREDDIT] = DEFAULT_SUBREDDIT
}
}
#OptIn(ExperimentalCoroutinesApi::class)
val articles = savedStateHandle.getLiveData<String>(KEY_SUBREDDIT)
.asFlow()
.flatMapLatest {
newsRepository.getBreakingNewsStream(it)
}.cachedIn(viewModelScope)
private val _userMessage = MutableStateFlow<String?>(null)
private val _isLoading = MutableStateFlow(false)
private val _newsArticles = articles
val uiState: StateFlow<NewsItemListUiState> = combine(
_isLoading, _userMessage, _newsArticles
) { isLoading, userMessage, newsArticles ->
NewsItemListUiState(
news = newsArticles,
isLoading = isLoading,
userMessage = userMessage
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = NewsItemListUiState(isLoading = true)
)
fun refresh() {
_isLoading.value = true
viewModelScope.launch {
newsRepository.refreshTasks()
_isLoading.value = false
}
}
}
NewsRepository
package com.sarmad.newsprism.data.repository
import androidx.paging.PagingData
import com.sarmad.newsprism.data.entities.NewsResponse
import kotlinx.coroutines.flow.Flow
import com.sarmad.newsprism.data.Result
import com.sarmad.newsprism.data.entities.Article
interface NewsRepository {
suspend fun getSearchedNewsStream(searchQuery: String, pageNumber: Int):
Flow<NewsResponse>
suspend fun getBreakingNewsStream(countryCode: String): Flow<PagingData<Article>>
}
NewsRepositoryImpl
package com.sarmad.newsprism.data.repository
import android.util.Log
import androidx.paging.*
import com.sarmad.newsprism.data.Result
import com.sarmad.newsprism.data.entities.Article
import com.sarmad.newsprism.data.entities.NewsResponse
import com.sarmad.newsprism.data.localdatasource.ArticleDao
import com.sarmad.newsprism.data.localdatasource.ArticleDatabase
import com.sarmad.newsprism.data.localdatasource.RemoteKeysDao
import com.sarmad.newsprism.data.paging.mediaters.NewsRemoteMediator
import com.sarmad.newsprism.data.remotedatasource.api.NewsApi
import com.sarmad.newsprism.utils.Constants.Companion.PAGING_CONFIG_PAGE_SIZE
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapLatest
import javax.inject.Inject
private const val TAG = "NewsRepositoryImpl"
class NewsRepositoryImpl #Inject constructor(
private val api: NewsApi,
private val articleDao: ArticleDao,
private val articleDatabase: ArticleDatabase,
private val remoteKeysDao: RemoteKeysDao,
) : NewsRepository {
override suspend fun getSearchedNewsStream(
searchQuery: String,
pageNumber: Int
): Flow<NewsResponse> = flow {
val searchedNewsResponse = api.searchNews(searchQuery, pageNumber)
if (searchedNewsResponse.isSuccessful) searchedNewsResponse.body()
?.let { newsList -> emit(newsList) }
else emptyFlow<NewsResponse>()
}
#OptIn(ExperimentalPagingApi::class)
override suspend fun getBreakingNewsStream(
countryCode: String
): Flow<PagingData<Article>> {
return Pager(
config = PagingConfig(
pageSize = PAGING_CONFIG_PAGE_SIZE
),
remoteMediator = NewsRemoteMediator(articleDatabase, articleDao, remoteKeysDao, api),
pagingSourceFactory = { articleDao.getNewsStream() }
).flow
}
}
NewsRemoteMadiator
package com.sarmad.newsprism.data.paging.mediaters
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.sarmad.newsprism.data.remotedatasource.api.NewsApi
import com.sarmad.newsprism.data.entities.Article
import com.sarmad.newsprism.data.entities.ArticleRemoteKey
import com.sarmad.newsprism.data.localdatasource.ArticleDao
import com.sarmad.newsprism.data.localdatasource.ArticleDatabase
import com.sarmad.newsprism.data.localdatasource.RemoteKeysDao
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.ceil
#OptIn(ExperimentalPagingApi::class)
class NewsRemoteMediator #Inject constructor(
private val articleDatabase: ArticleDatabase,
private val articleDao: ArticleDao,
private val remoteKeysDao: RemoteKeysDao,
private val api: NewsApi
) : RemoteMediator<Int, Article>() {
override suspend fun initialize(): InitializeAction {
val newsCacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
val isSkipRefresh = remoteKeysDao.getLastUpdateTime()?.let {
System.currentTimeMillis() - it >= newsCacheTimeout
}
return if (isSkipRefresh == true) {
InitializeAction.SKIP_INITIAL_REFRESH
} else {
InitializeAction.LAUNCH_INITIAL_REFRESH
}
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Article>
): MediatorResult {
return try {
val currentPage = when (loadType) {
LoadType.REFRESH -> {
val remoteKey = getRemoteKeyClosestToCurrentPosition(state)
remoteKey?.nextPage?.minus(1) ?: 1
}
LoadType.PREPEND -> {
val remoteKey = getRemoteKeyForFirstItem(state)
val prevPage = remoteKey?.prevPage ?: return MediatorResult.Success(
remoteKey != null
)
prevPage
}
LoadType.APPEND -> {
val remoteKey = getRemoteKeyForLastItem(state)
val nextPage =
remoteKey?.nextPage
?: return MediatorResult.Success(remoteKey != null)
nextPage
}
}
val response = api.getBreakingNews("us", currentPage)
val totalPages = response.body()?.totalResults?.toDouble()?.div(20)?.let { pages ->
ceil(pages)
}?.toInt()
val endOfPaginationReached = totalPages == currentPage
val nextPage = if (endOfPaginationReached) null else currentPage.plus(1)
val prevPage = if (currentPage == 1) null else currentPage.minus(1)
articleDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
articleDao.deleteAllArticles()
remoteKeysDao.deleteAllArticleRemoteKeys()
}
response.body()?.let { response ->
val keys = articleDao.insertAll(response.articles)
val mappedKeysToArticles = keys.map { key ->
ArticleRemoteKey(
id = key.toInt(),
nextPage = nextPage,
prevPage = prevPage,
modifiedAt = System.currentTimeMillis()
)
}
remoteKeysDao.insertArticleRemoteKeys(mappedKeysToArticles)
}
}
MediatorResult.Success(endOfPaginationReached)
} catch (ex: java.lang.Exception) {
return MediatorResult.Error(ex)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, Article>
): ArticleRemoteKey? {
return state.anchorPosition?.let { pos ->
state.closestItemToPosition(pos)?.id?.let { id ->
remoteKeysDao.getArticleRemoteKey(id)
}
}
}
private suspend fun getRemoteKeyForFirstItem(
state: PagingState<Int, Article>
): ArticleRemoteKey? {
return state.pages.firstOrNull {
it.data.isNotEmpty()
}?.data?.firstOrNull().let {
it?.let { it1 -> remoteKeysDao.getArticleRemoteKey(it1.id) }
}
}
private suspend fun getRemoteKeyForLastItem(
state: PagingState<Int, Article>
): ArticleRemoteKey? {
return state.pages.lastOrNull {
it.data.isNotEmpty()
}?.data?.lastOrNull().let {
it?.let { it1 -> remoteKeysDao.getArticleRemoteKey(it1.id) }
}
}
}
NewsViewModelTest
package com.sarmad.newsprism.news.ui
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.sarmad.newsprism.data.remotedatasource.api.NewsApi
import com.sarmad.newsprism.data.repository.NewsRepository
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
internal class NewsViewModelTest {
#Mock
lateinit var newsRepository: NewsRepository
#Mock
private lateinit var newsApi: NewsApi
private lateinit var newsViewModel: NewsViewModel
#OptIn(DelicateCoroutinesApi::class)
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
#OptIn(ExperimentalCoroutinesApi::class)
#Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(mainThreadSurrogate)
newsViewModel = NewsViewModel(newsRepository)
}
#OptIn(ExperimentalCoroutinesApi::class)
#After
fun tearDown() {
Dispatchers.resetMain() // reset the main dispatcher to the original Main dispatcher
mainThreadSurrogate.close()
}
#OptIn(ExperimentalCoroutinesApi::class)
#Test
fun test_getNewsFlow() = runTest {
newsViewModel.getNewsStream("us")
assertEquals(true, newsViewModel.newsFlow.value.isLoading)
advanceUntilIdle()
assertEquals(
false,
newsViewModel.newsFlow.value.isLoading
)
assertNotNull(
newsViewModel.newsFlow.value.news
)
}
}
The best way to check a call is using a Mock.
Create an interface for your NewsViewModel like INewsViewModel and inject it by using the constructor or the Setup. Depending on your Mock package it can be created like:
//Implementation using Moq
Mock<INewsViewModel> mock = new Mock<INewsViewModel>();
mock.Setup(m => m.getNewsStream());
// Your test
mock.VerifyAll();
Moq also allows to create a Mock when the mocked class has an empty constructor.
I am creating an app that right now is just supposed to be able to use Firebase authentication with mvvm as a standard to get some kind of seperation of concerns. So i have a database class that uses Firebase and gets an injection from Dagger hilt to be able to use Authentication from firebase. Now i am trying to check if the user is logged in. So i type the auth.getCurrentUser(). This does only give me the possibility to check if a user exist and with firebase when you check this and you have deleted the user while you are testing it does not update the value so it's still logged in for another hour when you have deleted the user. If you check around the internet about this you get the answer to use the authstatelistener. My question is though how to use this together with mvvm? is there a way to do this when i have my clases seperated by a viewmodel a repository and a database.
My classes look like this right now.
Database: //This has a comment written in it that has some use for the problem
import com.google.android.gms.tasks.Task
import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.lang.Exception
import javax.inject.Inject
import javax.security.auth.callback.Callback
class FirDatabase #Inject constructor(var auth : FirebaseAuth) {
suspend fun register(user: User) : Task<AuthResult>{
return auth.createUserWithEmailAndPassword(user.email, user.password)
}
suspend fun checkIfUserExist() : Boolean? {
//i would like to be able to check it right here somehow
println("Currentuser " + auth.currentUser?.uid)
return auth.currentUser != null
}
}
Repository:
import androidx.lifecycle.MutableLiveData
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks.await
import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AuthRepository #Inject constructor (private val database: FirDatabase) : IAuthRepository {
override suspend fun register(user: User) : Resource<AuthResult> {
return try{
val response = database.register(user)
val result = response.result
if(response.isSuccessful && result != null){
Resource.Success(result)
}else{
Resource.Error(response.exception?.message.toString())
}
}catch (e: Exception){
Resource.Error(e.message ?: "An Error occurred")
}
}
override suspend fun CheckIfloggedIn() : Resource<Boolean>{
return try{
val user = database.checkIfUserExist()
if(user != false){
Resource.IsLoggedIn("User is already logged in" )
}else{
Resource.IsNotLoggedIn("User is not logged in")
}
}catch(e: Exception){
Resource.Error(e.message ?: "An Error occurred")
}
}
}
ViewModel:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
#HiltViewModel
class AuthViewModel #Inject constructor(private val repository: AuthRepository, private val dispatchers: DispatcherProvider) : ViewModel(){
sealed class AuthEvent{
class Success(val result: String): AuthEvent()
class Failure(val errorText: String): AuthEvent()
object Loading : AuthEvent()
object LoggedIn : AuthEvent()
object NotRegistered : AuthEvent()
object NotLoggedIn : AuthEvent()
object Empty : AuthEvent()
}
private val _registering = MutableStateFlow<AuthEvent>(AuthEvent.Empty)
val registering : StateFlow<AuthEvent> = _registering
private val _checkUserIsLoggedIn = MutableStateFlow<AuthEvent>(AuthEvent.Empty)
val checkUserIsLoggedIn : StateFlow<AuthEvent> = _checkUserIsLoggedIn
fun register(user: User){
viewModelScope.launch(dispatchers.io) {
_registering.value = AuthEvent.Loading
when(val authResponse = repository.register(user)){
is Resource.Error -> _registering.value = AuthEvent.Failure(authResponse.message!!)
is Resource.Success -> {
val response = authResponse.data!!.user
_registering.value = AuthEvent.Success("Success")
}
}
}
}
fun CheckIfUserIsLoggedIn()
{
viewModelScope.launch(dispatchers.io) {
when(val isUserLoggedIn = repository.CheckIfloggedIn()){
is Resource.IsLoggedIn -> _checkUserIsLoggedIn.value = AuthEvent.LoggedIn
is Resource.IsNotLoggedIn -> _checkUserIsLoggedIn.value = AuthEvent.NotLoggedIn
is Resource.Error -> _checkUserIsLoggedIn.value = AuthEvent.Failure(isUserLoggedIn.message!!)
}
}
}
}
I have followed tutorials from this dude https://www.youtube.com/watch?v=ct5etYgB5pQ
and i have already seen alot of the documentation on this page like this for example Firebase: how to check if user is logged in?
and here How does the firebase AuthStateListener work?. So with further investigations into the answer you gave me i have come up with this solution... but it is not really working? why is this?
Repository function:
override fun CheckIfloggedIn() = callbackFlow<Resource<Boolean>>{
val isUserLoggedIn = flow<Resource<Boolean>>{
database.checkIfUserExist().collect { isLoggedIn ->
if(isLoggedIn){
Resource.Success(isLoggedIn)
}else{
Resource.Error("User is not logged in")
}
}
}
}
Database function:
fun checkIfUserExist() = callbackFlow {
val authStatelistener = FirebaseAuth.AuthStateListener {auth ->
trySend(auth.currentUser == null)
}
auth.addAuthStateListener(authStatelistener)
awaitClose {
auth.removeAuthStateListener(authStatelistener)
}
}
Hello I have a unit test which was using Mockito, I converted most part of it to use Mockk except one part where I want to create a Observer of android lifecycle
Mockito version which works
#Mock
private lateinit var dataObserver: Observer<Result<List<Character>>>
Mockk version which does not work
private var dataObserver: Observer<Result<List<Character>>> = mockk(relaxed = true)
My test case fails with the following error when I use Mockk for the above and passes when I use Mockito version
error message
java.lang.AssertionError: Verification failed: call 1 of 1: Observer(#2).onChanged(eq(Success([Character(name=myName, img=image, occupation=[], status=status, nickname=nickName, appearance=[])])))) was not called
at io.mockk.impl.recording.states.VerifyingState.failIfNotPassed(VerifyingState.kt:66)
at io.mockk.impl.recording.states.VerifyingState.recordingDone(VerifyingState.kt:42)
at io.mockk.impl.recording.CommonCallRecorder.done(CommonCallRecorder.kt:47)
at io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:60)
at io.mockk.impl.eval.VerifyBlockEvaluator.verify(VerifyBlockEvaluator.kt:30)
at io.mockk.MockKDsl.internalCoVerify(API.kt:143)
at io.mockk.MockKKt.coVerify(MockK.kt:175)
at io.mockk.MockKKt.coVerify$default(MockK.kt:172)
at com.example.breakingbad.MainActivityViewModelTest$fetchCharacters$1.invokeSuspend(MainActivityViewModelTest.kt:76)
Full test case
package com.example.breakingbad
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import com.example.breakingbad.data.DataRepository
import com.example.breakingbad.model.Character
import com.example.breakingbad.viewModel.MainActivityViewModel
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
//#RunWith(MockitoJUnitRunner::class)
#ExperimentalCoroutinesApi
class MainActivityViewModelTest {
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testDispatcher)
private val dataRepository: DataRepository = mockk(relaxed = true)
private val mainActivityViewModel = MainActivityViewModel(dataRepository)
// #Mock
// private lateinit var dataObserver: Observer<Result<List<Character>>>
private var dataObserver: Observer<Result<List<Character>>> = mockk(relaxed = true)
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
#After
fun cleanup() {
Dispatchers.resetMain()
testCoroutineScope.cleanupTestCoroutines()
}
#Test
fun fetchCharacters() {
testCoroutineScope.launch {
coEvery { dataRepository.getCharacters() } returns Result.success(arrayListOf(Character(
name = "myName",
img = "image",
occupation = arrayListOf(),
status = "status",
nickname = "nickName",
appearance = arrayListOf()
)))
mainActivityViewModel.fetchCharacters()
coVerify { dataRepository.getCharacters() }
coVerify { dataObserver.onChanged(
Result.success(listOf(Character (
name = "myName",
img = "image",
occupation = arrayListOf(),
status = "status",
nickname = "nickName",
appearance = arrayListOf()
)))
) }
mainActivityViewModel.charactersLiveData.removeObserver(dataObserver)
}
}
}
how to make the test case pass using Mockk? what am I doing wrong?
Edit
ViewModel
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
): ViewModel() {
private val _charactersLiveData = MutableLiveData<Result<ArrayList<Character>>>()
val charactersLiveData: LiveData<Result<ArrayList<Character>>> = _charactersLiveData
fun fetchCharacters() {
viewModelScope.launch(Dispatchers.IO) {
_charactersLiveData.postValue(dataRepository.getCharacters())
}
}
}
An a form to test a liveData is capturing the data passing through by the observer.
PD: In your case maybe he forgot add observerForever to livedata.
Anyway I leave an example:
private val dataObserver: Observer<Result<List<Character>>> = mockk()
private val argumentCaptor: CapturingSlot<Result<List<Character>> = slot()
#Test
fun fetchCharacters() = runBlockingTest {
//Move to #Before
mainActivityViewModel.charactersLiveData.observerForever(dataObserver)
coEvery { dataRepository.getCharacters() } returns Result.success(arrayListOf(Character(
name = "myName",
img = "image",
occupation = arrayListOf(),
status = "status",
nickname = "nickName",
appearance = arrayListOf()
)))
mainActivityViewModel.fetchCharacters()
coVerify { dataRepository.getCharacters() }
verify { dataObserver.onChanged(capture(argumentCaptor)) }
with(argumentCaptor){
//Here can be assert data, example:
assert(this is Result.Success)
assertEquals(YourData, this.data) //validation according to your data structure
}
//Move to #After
mainActivityViewModel.charactersLiveData.removeObserver(dataObserver)
}
Android kotlin coroutine retrofit.
I want to get the value from the getPropeties to insert it in database. I Need a help for this? I need the value to be an instance of User not the unit value. My viewModel class is given below.
import android.app.Application
import android.content.Context
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.android.marsrealestate.database.AppDatabase
import com.example.android.marsrealestate.database.User
import com.example.android.marsrealestate.database.UserDao
import com.example.android.marsrealestate.network.UsersApi
import com.example.android.marsrealestate.network.UsersProperty
import kotlinx.coroutines.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class OverviewViewModel(val database: UserDao,
application: Application): ViewModel() {
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(
viewModelJob + Dispatchers.Main )
private var user = MutableLiveData<User?>()
// The internal MutableLiveData String that stores the most recent response
private val _response = MutableLiveData<String>()
// The external immutable LiveData for the response String
val response: LiveData<String>
get() = _response
init {
getUsersProperties()
}
private fun getUsersProperties(){
coroutineScope.launch {
var getPropertiesDeferred =
UsersApi.retrofitService.getProperties()
try {
var listResult = getPropertiesDeferred.await()
//database.insertUser(listResult)
_response.value =
"Success: ${listResult} Mars properties retrieved"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Thanks
You are using launch,
Launch is used to perform asynchronous fire and forget type of
operations where you are not interested in the result of operation.
Instead, you can use async,
Async is used to perform asynchronous computation where you expect a
result of the computation in the future
private fun getUsersProperties() =
coroutineScope.async {
var getPropertiesDeferred =
UsersApi.retrofitService.getProperties()
try {
var listResult = getPropertiesDeferred.await()
//database.insertUser(listResult)
_response.value =
"Success: ${listResult} Mars properties retrieved"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
// =================================================
// ========= Return whatever result you want =======
// =================================================
}
can you also show what is the type signature of getProperties?