in a class which extends BroadcasrReceiver, I want to access a repository function.
I get an error lateinit property chargeSessionRepo has not been initialized.
I have specified #Inject, what else needs to be done, so I can access the function in ChargeSessionRepository
class CustomNotificationListener : BroadcastReceiver() {
#Inject
lateinit var chargeSessionRepo: ChargeSessionRepository
override fun onReceive(context: Context, intent: Intent) {
Log.d("fcmService", "onReceive ${intent.getStringExtra("onClickListener")}")
when(intent.getStringExtra("onClickListener")) {
"approve" -> {
approvePlugin(true)
}
"deny" -> {
approvePlugin(false)
}
}
}
private fun approvePlugin(isApproved: Boolean) {
GlobalScope.launch {
try {
val chargeStatusUpdate = chargeSessionRepo.getChargeStatusUpdate().data
val chargeStatus = chargeStatusUpdate?.firstOrNull()
chargeStatus?.sessionId?.let {
chargeSessionRepo.approvePlugIn(it, isApproved)
}
} catch (exception: Exception) {
Log.d("fcmService", "exception: ${exception}") // lateinit property chargeSessionRepo has not been initialized
}
}
}
}
ChargesessionRepository
#Singleton
class ChargeSessionRepository #Inject constructor(
private val chargeSessionService: ChargeSessionService,
private val chargeSessionDao: ChargeSessionDao
) : Repository() {
var lastChargeStatus: List<ChargeStatusDTO>? = null
private set
suspend fun getChargeStatusUpdate(): Resource<List<ChargeStatusDTO>> {
val resource = getResource { chargeSessionService.getChargeStatus() }
lastChargeStatus = resource.data
return resource
}
}
could you please suggest how to do this
Thanks
R
You may have forgotten to put #AndroidEntryPointon top of your broadcast receiver. if you are using hilt.
Related
I followed this tutorial link
But I met a problem that "kotlin.UninitializedPropertyAccessException: lateinit property splashViewModel has not been initialized"
Here is my code
#Module
#InstallIn(SingletonComponent::class)
object MainModule {
#Provides
#Singleton
fun provideDataStoreRepository(
#ApplicationContext context: Context
) = DataStoreRepository(context = context)
}
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "on_boarding_pref")
class DataStoreRepository(context: Context) {
private object PreferencesKey {
val onBoardingKey = booleanPreferencesKey(name = "on_boarding_completed")
}
private val dataStore = context.dataStore
suspend fun saveOnBoardingState(completed: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKey.onBoardingKey] = completed
}
}
fun readOnBoardingState(): Flow<Boolean> {
return dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val onBoardingState = preferences[PreferencesKey.onBoardingKey] ?: false
onBoardingState
}
}
}
class SplashViewModel #Inject constructor(
private val repository: DataStoreRepository
) : ViewModel() {
private val _isLoading: MutableState<Boolean> = mutableStateOf(true)
val isLoading: State<Boolean> = _isLoading
private val _startDestination: MutableState<String> = mutableStateOf(Screen.OnboardingFirstScreen.route)
val startDestination: State<String> = _startDestination
init {
viewModelScope.launch {
repository.readOnBoardingState().collect { completed ->
if (completed) {
_startDestination.value = Screen.MainScreen.route
} else {
_startDestination.value = Screen.OnboardingFirstScreen.route
}
}
_isLoading.value = false
}
}
}
And in my main activity
class MainActivity : ComponentActivity() {
#Inject
lateinit var splashViewModel: SplashViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen().setKeepOnScreenCondition {
!splashViewModel.isLoading.value
}
setContent{
BottomNavWithBadgesTheme {
val screen by splashViewModel.startDestination
....
}
}
It turned out MainModule object have never been used. Is that problem? I'm new to jetpack data store, I just followed it, so I don't know where is the problem and how to fix it. Thank you in advance.
Firstly, it's not about data store. It is about dependency injection. You are trying to get the data from viewmodel when it is not initialized.
To solve the problem:
Mark your viewmodel class with #HiltViewModel annotation
Remove lateinit var keyword and #Inject annotation from viewmodel in your MainActivity
Your viewmodel must be initialized in onCreate function like that:
viewModel: SplashViewModel = hiltViewModel()
I'm Trying to make a viewmodel scoped to my application to control logic related to showing of not showing pin in multi activity app .
I've used AndroidViewModel to pass the application to it and here is the class for AppViewModel
#HiltViewModel
class AppViewModel #Inject constructor(
private val getUserPassCodeUseCase: GetUserPassCodeUseCase,
private val isPasscodeInputUseCase: IsPasscodeInputUseCase,
private val clearAllDataUseCase: ClearAllDataUseCase,
#ApplicationContext private val context: Context
) : AndroidViewModel((context as App)) {
private val _openPin = MutableSharedFlow<Long>()
val openPin = _openPin.asSharedFlow()
// uptime in millis
private var time: Long = 0
private var restoreStatus = RestoreStatus.EMPTY
private var isPasscode = false
private var passCode = ""
init {
// get user status
}
private fun checkIfShouldLock() {
viewModelScope.launch {
isPasscode = withContext(IO) {
isPasscodeInputUseCase()
}
val userHasAccount = (restoreStatus == RestoreStatus.ID_SUBMISSION
|| restoreStatus == RestoreStatus.TERMS_AND_CONDITION
|| restoreStatus == RestoreStatus.ACTIVATE_CARD
|| restoreStatus == RestoreStatus.FULL_NAME
|| restoreStatus == RestoreStatus.COMPLETED)
if (true)
_openPin.emit (System.currentTimeMillis() )
}
}
fun onResume() {
updatePasscode()
if (!isPasscode) {
time = 0
return
}
val now = SystemClock.elapsedRealtime()
when {
time == 0L -> {
// remember first value
time = now
}
// check is session expired
now - time > sessionExpiredTime -> {
time = now
when (restoreStatus) {
RestoreStatus.COMPLETED -> checkIfShouldLock()
RestoreStatus.EMPTY -> {}
else -> {}
}
}
else -> {
time = now
}
}
}
fun onPause() {
updatePasscode()
viewModelScope.launch {
delay(Constants.PASSCODE_DELAY)
if (!isPasscode) {
time = 0
return#launch
}
// start "timer"
time = SystemClock.elapsedRealtime()
}
}
private fun updatePasscode() {
viewModelScope.launch {
isPasscode = withContext(IO) {
isPasscodeInputUseCase()
}
}
}
fun logout() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
clearAllDataUseCase()
}
}
}
companion object {
const val MAIN_VIEW_MODEL_TAG = "AppViewModel"
}
}
and here is my application class and how i try to access the viewmodel
#HiltAndroidApp
class App : Application(), Application.ActivityLifecycleCallbacks,
Configuration.Provider {
#Inject
lateinit var workerFactory: HiltWorkerFactory
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
// uptime in millis
private var time: Long = 0
var appViewModel : AppViewModel ?=null
var currentActivity : String ?=null
override fun onCreate() {
super.onCreate()
AndroidThreeTen.init(this)
appViewModel = ViewModelProvider.AndroidViewModelFactory(this).create(AppViewModel::class.java)
appViewModel?.openPin?.onEach {
// if (authToken.isNotEmpty())
when (currentActivity) {
SplashActivity::class.java.name,
PinActivity::class.java.name -> Unit
else -> {
startActivity(Intent(this, PinActivity::class.java).apply {
// flag of should end with result or not
// putExtra(Constants.IS_CAME_FROM_BACKGROUND, true)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}
}
}
setupCrashlytics()
if (BuildConfig.DEBUG)
Timber.plant(Timber.DebugTree())
else
Timber.plant(CrashReportingTree())
DyScan.init(this, Constants.DYSCAN_API_KEY)
registerActivityLifecycleCallbacks(this)
}
private fun setupCrashlytics() {
with(FirebaseCrashlytics.getInstance()) {
setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
}
}
private fun isDeviceRooted(): Boolean {
var process: Process? = null
return try {
process = Runtime.getRuntime().exec("su")
true
} catch (e: Exception) {
Timber.i(e, "Rooted device command exception")
false
} finally {
if (process != null) {
try {
process.destroy()
} catch (e: Exception) {
Timber.i(e, "Rooted device command close exception")
}
}
}
}
private fun hideSystemBars(activity: Activity) {
val windowInsetsController =
ViewCompat.getWindowInsetsController(activity.window.decorView) ?: return
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE
windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars())
}
override fun onActivityCreated(activity: Activity, p1: Bundle?) {
currentActivity = activity.localClassName
if (isDeviceRooted()) {
Toast.makeText(
activity,
getString(R.string.rooted_device_message),
Toast.LENGTH_SHORT
).show()
activity.finishAffinity()
}
}
override fun onActivityStarted(p0: Activity) {
appViewModel?.onResume()
}
override fun onActivityResumed(p0: Activity) {}
override fun onActivityPaused(p0: Activity) {}
override fun onActivityStopped(activity: Activity) {
appViewModel?.onPause()
}
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {}
override fun onActivityDestroyed(p0: Activity) {}
}
i keep getting RuntimeException: Cannot create an instance of class x.x.AppViewModel
2022-03-08 22:29:44.189 10889-10889/com.x.x E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.x.x, PID: 10889
java.lang.RuntimeException: Unable to create application com.x.x.App: java.lang.RuntimeException: Cannot create an instance of class com.x.x.AppViewModel
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6991)
at android.app.ActivityThread.access$1700(ActivityThread.java:274)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2093)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:233)
at android.app.ActivityThread.main(ActivityThread.java:8010)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)
Caused by: java.lang.RuntimeException: Cannot create an instance of class com.x.x.AppViewModel
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:230)
at com.x.x.App.onCreate(App.kt:54)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1208)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6986)
at android.app.ActivityThread.access$1700(ActivityThread.java:274)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2093)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:233)
at android.app.ActivityThread.main(ActivityThread.java:8010)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)
Caused by: java.lang.NoSuchMethodException: com.x.x.AppViewModel.<init> [class android.app.Application]
at java.lang.Class.getConstructor0(Class.java:2332)
at java.lang.Class.getConstructor(Class.java:1728)
at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:228)
at com.x.x.App.onCreate(App.kt:54)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1208)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6986)
at android.app.ActivityThread.access$1700(ActivityThread.java:274)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2093)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:233)
at android.app.ActivityThread.main(ActivityThread.java:8010)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)
What i am doing wrong in this implementation ?
Is this the right way to implement a viewModel scoped to application ?
From what I can tell, something is trying to call a no argument constructor on your ViewModel, but you have only defined a constructor that takes arguments.
You have arguments in viewmodel constructor. You will have to extend viewmodel factory and handle the parameters passed. Later pass the required parameter values to custom viewmodel factory instance. Finally use the custom view model factory instance in ViewModelProvider.AndroidViewModelFactory(this ,customInstance).create (AppViewModel::class.java)
I already create some unit test for my view model. but when I println() the result it always return State Loading.. I have tried to read some article and cek in other source code but I'm still not found the answer.
Here is my code from ViewModel :
class PredefineViewModel() : ViewModel() {
private var predefineRepository: PredefineRepository? = PredefineRepository()
private val _predefined = MutableLiveData<String>()
val predefined: LiveData<Resource<Payload<Predefine>>> =
Transformations.switchMap(_predefined) {
predefineRepository?.predefine()
}
fun predefined() {
_predefined.value = "predefined".random().toString()
}
}
Here is my Repository
class PredefineRepository() {
private val api: PredefineApi? = PredefineApi.init()
fun predefine(): BaseMutableLiveData<Predefine> {
val predefine: BaseMutableLiveData<Predefine> = BaseMutableLiveData()
api?.let { api ->
predefine.isLoading()
api.predefined().observe()?.subscribe({ response ->
response?.let { resource ->
predefine.isSuccess(resource)
}
}, { error ->
predefine.isError(error)
})
}
return predefine
}
}
Here is my Resources State :
data class Resource<T>(var status: Status? = null, var meta: Meta? = null, var payload: T? =null) {
companion object {
fun <T> success(data: T?, meta: Meta): Resource<T> {
return Resource(Status.SUCCESS, meta, data)
}
fun <T> error(data: T?, meta: Meta): Resource<T> {
return Resource(Status.ERROR, meta, data)
}
fun <T> loading(data: T?, meta: Meta): Resource<T> {
return Resource(Status.LOADING, null, null)
}
}
}
UPDATE TEST CLASS
And, This is sample I try to print and check value from my live data view model :
class PredefineViewModelTest {
#get:Rule
val taskExecutorRule = InstantTaskExecutorRule()
private lateinit var viewModel: PredefineViewModel
private lateinit var repository: PredefineRepository
private lateinit var api: Api
#Before
fun setUp() {
api = Networks().bridge().create(Api::class.java)
repository = PredefineRepository()
viewModel = PredefineViewModel()
}
#Test
fun test_predefined(){
val data = BaseMutableLiveData<Predefine>()
val result = api.predefined()
result.test().await().assertComplete()
result.subscribe {
data.isSuccess(it)
}
`when`(repository.predefine()).thenReturn(data)
viewModel.predefined()
viewModel.predefined.observeForever {
println("value: $it")
println("data: ${data.value}")
}
}
}
UPDATE LOG Results
Why the result from my predefined always:
value: Resource(status=LOADING, meta=null, payload=null, errorData=[])
data: Resource(status=SUCCESS, meta=Meta(code=200, message=success, error=null), payload= Data(code=200, message=success, errorDara =[])
Thank You..
You would require to mock your API response. The unit test won't run your API actually you have to mock that. Please have a look at the attached snippet, It will give you a basic idea of how you can achieve that.
ViewModel:
class MainViewModel(val repository: Repository) : ViewModel() {
fun fetchData(): LiveData<Boolean> {
return Transformations.map(repository.getData()) {
if (it.status == 200) {
true
} else {
false
}
}
}
}
Repo:
open class Repository {
open fun getData() : LiveData<MainModel> {
return MutableLiveData(MainModel(10, 200))
}
}
Test Class:
#RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
lateinit var mainModel: MainViewModel
#Rule
#JvmField
var rule: TestRule = InstantTaskExecutorRule()
#Mock
lateinit var repo: Repository
init {
MockitoAnnotations.initMocks(this)
}
#Before
fun setup() {
mainModel = MainViewModel(repo)
}
#Test
fun fetchData_success() {
val mainModelData = MainModel(10, 200)
`when`(repo.getData()).thenReturn(MutableLiveData(mainModelData))
mainModel.fetchData().observeForever {
Assert.assertTrue(it)
}
}
#Test
fun fetchData_failure() {
val mainModelData = MainModel(10, 404)
`when`(repo.getData()).thenReturn(MutableLiveData(mainModelData))
mainModel.fetchData().observeForever {
Assert.assertFalse(it)
}
}
}
I couldn't see your API mock. Your initial status is loading inside LiveData.
{ response ->
response?.let { resource ->
predefine.isSuccess(resource)
}
block is not executing during the test.
In my application i am following MVP pattern, in this i want to make junit test cases for presenter and model(interactor) classes to validate the business logic.
Below is the code i have written for presenter and model, and i also also written a junit test case as mentioned below.
LoginPresenterImpl.kt
class LoginPresenterImpl : LoginPresenter, LoginResponseCallback {
lateinit var loginIntegractor:LoginIntegractor
override fun loginSuccess(user: User) {
loginView.hidProgress()
loginView.loginSuucces(user)
}
override fun loginFailed(errorMessage: String) {
loginView.hidProgress()
loginView.loginFailed(errorMessage)
}
lateinit var loginView:LoginView
constructor(context: Context,loginView: LoginView){
this.loginView = loginView;
loginIntegractor = LoginInteractorImpl(context,this);
}
override fun login(userName: String, password: String) {
loginView.showProgress();
loginIntegractor.login(userName,password);
}
}
LoginInteractorImpl.kt This file consist of business logic of login
class LoginInteractorImpl : LoginIntegractor {
val TAG:String = LoginInteractorImpl::class.java.simpleName;
var loginResponseCallback: LoginResponseCallback;
var context: Context? = null;
constructor(context: Context,loginResponseCallback: LoginResponseCallback){
this.context = context;
this.loginResponseCallback = loginResponseCallback;
}
constructor(loginResponseCallback: LoginResponseCallback){
this.loginResponseCallback = loginResponseCallback;
}
override fun login(username: String, password: String) {
if(username.trim().isBlank()){
loginResponseCallback.loginFailed("Please enter username");
}
else if(password.trim().isBlank()){
loginResponseCallback.loginFailed("Please enter password");
}
else{
val apiService:ApiService = ApiService.Factory.create();
val jsonObject = JSONObject();
jsonObject.put("username",username);
jsonObject.put("password",password);
val call:Call<LoginResponse> = apiService.login(jsonObject.toString())
call.enqueue(object : Callback<LoginResponse> {
override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
Log.d(TAG, "login success")
if (response != null) {
val status = response.body()!!.getStatus()
if (status == 0) {
loginResponseCallback.loginSuccess(response.body()!!.getUser())
} else {
loginResponseCallback.loginFailed(response.body()!!.getMessage())
}
}
}
override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
Log.d(TAG, "login failed")
loginResponseCallback.loginFailed("Something went wrong while login")
}
})
}
}
}
LoginInteractorTest.kt This is juit test case file.
class LoginInteractorTest {
var loginIntegractor:LoginIntegractor? = null
#Mock
private lateinit var callback: LoginResponseCallback
#Captor
private lateinit var argumentCaptor:ArgumentCaptor<LoginResponseCallback>;
#Captor
private lateinit var nameCapcture:ArgumentCaptor<String>;
#Captor
private lateinit var pwdcapcture:ArgumentCaptor<String>;
private lateinit var user: User;
#Before
fun setUp(){
callback = mock()
user = mock()
val captor = argumentCaptor<() -> Unit>()
nameCapcture = ArgumentCaptor.forClass(String::class.java)
pwdcapcture = ArgumentCaptor.forClass(String::class.java)
loginIntegractor = LoginInteractorImpl(callback)
}
#Test
fun testLogin() {
MockitoAnnotations.initMocks(this)
loginIntegractor?.login("ashok","narra")
// verify(loginIntegractor?.login(nameCapcture.capture(),pwdcapcture.capture()))
// argumentCaptor.value.loginSuccess(ArgumentMatchers.any(User::class.java))
Mockito.verify(callback).loginSuccess(ArgumentMatchers.any(User::class.java));
}
}
test case fails saying java.lang.IllegalStateException: ArgumentMatchers.any(User::class.java) must not be null". Can anyone suggest how to we implement junit test cases for presenter/model classes in android using kotlin?
I had the same issue recently and to be honest I couldn't solve it and ended up using mockito-kotlin.
Instead of
Mockito.verify(callback).loginSuccess(ArgumentMatchers.any(User::class.java));
You can simply write:
Mockito.verify(callback).loginSuccess(any())
Where any() comes from mockito kotlin. There's usually no need to specify the type, but in case you need it you can always do it like any<User>()
I want to write a unitTest for my viewModel class :
#RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
#get:Rule
var rule: TestRule = InstantTaskExecutorRule()
#Mock
private lateinit var context: Application
#Mock
private lateinit var api: SuperHeroApi
#Mock
private lateinit var dao: HeroDao
private lateinit var repository: SuperHeroRepository
private lateinit var viewModel: MainViewModel
private lateinit var heroes: List<Hero>
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
val localDataSource = SuperHeroLocalDataSource(dao)
val remoteDataSource = SuperHeroRemoteDataSource(context, api)
repository = SuperHeroRepository(localDataSource, remoteDataSource)
viewModel = MainViewModel(repository)
heroes = mutableListOf(
Hero(
1, "Batman",
Powerstats("1", "2", "3", "4", "5"),
Biography("Ali", "Tehran", "first"),
Appearance("male", "Iranian", arrayOf("1.78cm"), arrayOf("84kg"), "black", "black"),
Work("Android", "-"),
Image("url")
)
)
}
#Test
fun loadHeroes() = runBlocking {
`when`(repository.getHeroes(anyString())).thenReturn(Result.Success(heroes))
with(viewModel) {
showHeroes(anyString())
assertFalse(dataLoading.value!!)
assertFalse(isLoadingError.value!!)
assertTrue(errorMsg.value!!.isEmpty())
assertFalse(getHeroes().isEmpty())
assertTrue(getHeroes().size == 1)
}
}
}
I receive following Exception :
java.lang.NullPointerException
at com.sample.android.superhero.data.source.remote.SuperHeroRemoteDataSource$getHeroes$2.invokeSuspend(SuperHeroRemoteDataSource.kt:25)
at |b|b|b(Coroutine boundary.|b(|b)
at com.sample.android.superhero.data.source.SuperHeroRepository.getHeroes(SuperHeroRepository.kt:21)
at com.sample.android.superhero.MainViewModelTest$loadHeroes$1.invokeSuspend(MainViewModelTest.kt:68)
Caused by: java.lang.NullPointerException
at com.sample.android.superhero.data.source.remote.SuperHeroRemoteDataSource$getHeroes$2.invokeSuspend(SuperHeroRemoteDataSource.kt:25)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
And here is my RemoteDataSource class :
#Singleton
class SuperHeroRemoteDataSource #Inject constructor(
private val context: Context,
private val api: SuperHeroApi
) : SuperHeroDataSource {
override suspend fun getHeroes(query: String): Result<List<Hero>> = withContext(Dispatchers.IO) {
try {
val response = api.searchHero(query).await()
if (response.isSuccessful && response.body()?.response == "success") {
Result.Success(response.body()?.wrapper!!)
} else {
Result.Error(DataSourceException(response.body()?.error))
}
} catch (e: SocketTimeoutException) {
Result.Error(
DataSourceException(context.getString(R.string.no_internet_connection))
)
} catch (e: IOException) {
Result.Error(DataSourceException(e.message ?: "unknown error"))
}
}
}
When we use Rxjava we can create an Observable as simple as :
val observableResponse = Observable.just(SavingsGoalWrapper(listOf(savingsGoal)))
`when`(api.requestSavingGoals()).thenReturn(observableResponse)
How about Deferred in Coroutines? How can I test my method :
fun searchHero(#Path("name") name: String): Deferred<Response<HeroWrapper>>
The best way I've found to do this is to inject a CoroutineContextProvider and provide a TestCoroutineContext in test. My Provider interface looks like this:
interface CoroutineContextProvider {
val io: CoroutineContext
val ui: CoroutineContext
}
The actual implementation looks something like this:
class AppCoroutineContextProvider: CoroutineContextProvider {
override val io = Dispatchers.IO
override val ui = Dispatchers.Main
}
And a test implementation would look something like this:
class TestCoroutineContextProvider: CoroutineContextProvider {
val testContext = TestCoroutineContext()
override val io: CoroutineContext = testContext
override val ui: CoroutineContext = testContext
}
So your SuperHeroRemoteDataSource becomes:
#Singleton
class SuperHeroRemoteDataSource #Inject constructor(
private val coroutineContextProvider: CoroutineContextProvider,
private val context: Context,
private val api: SuperHeroApi
) : SuperHeroDataSource {
override suspend fun getHeroes(query: String): Result<List<Hero>> = withContext(coroutineContextProvider.io) {
try {
val response = api.searchHero(query).await()
if (response.isSuccessful && response.body()?.response == "success") {
Result.Success(response.body()?.wrapper!!)
} else {
Result.Error(DataSourceException(response.body()?.error))
}
} catch (e: SocketTimeoutException) {
Result.Error(
DataSourceException(context.getString(R.string.no_internet_connection))
)
} catch (e: IOException) {
Result.Error(DataSourceException(e.message ?: "unknown error"))
}
}
}
When you inject the TestCoroutineContextProvider you can then call methods such as triggerActions() and advanceTimeBy(long, TimeUnit) on the testContext so your test would look something like:
#Test
fun `test action`() {
val repository = SuperHeroRemoteDataSource(testCoroutineContextProvider, context, api)
runBlocking {
when(repository.getHeroes(anyString())).thenReturn(Result.Success(heroes))
}
// NOTE: you should inject the coroutineContext into your ViewModel as well
viewModel.getHeroes(anyString())
testCoroutineContextProvider.testContext.triggerActions()
// Do assertions etc
}
Note you should inject the coroutine context provider into your ViewModel as well. Also TestCoroutineContext() has an ObsoleteCoroutinesApi warning on it as it will be refactored as part of the structured concurrency update, but as of right now there is no change or new way of doing this, see this issue on GitHub for reference.