How to mock Android Lifecycle components? - android

I am trying to build an Android app following the recommended design structure.
Let's say, there is a UserRepository for handling the users. However, I would like to have certain settings in the app, for example "Show profile picture", "Sort by", etc. I would like to store these settings in a Room database, just like the Users.
According to my understanding, the cleanest way is to have a separate UserRepository and a SettingsRepository. And of course, the Settings should have a sort of Model, let's call it SettingsModel, in order to be able to retrieve the Settings as a Map for example. Note, that this is not a ViewModel, it has nothing to do with the UI.
Then, the UserRepository should implement its own business (handling the users), just like in the example linked above. Besides that, it also should have a dependency of that SettingsModel, so that it can easily retrieve the settings, which affect how the Users should be retrieved.
The SettingsModel needs to turn the "raw database data" into a map, so that I can reach the settings like this: settings.show_profile_pictures and settings.sort_by, etc.
To achieve this, I need to extract the data from the LiveData, which implies, that I need to observe that LiveData, so that I can update the Map, whenever the settings change.
And here comes the problem: the observe() method needs a LifecycleOwner, which I cannot provide in my tests.
1st attempt: mock with Mockito
It would be an instrumented test, becase that way I have access to an Activity, which is needed to retrieve the DAO.
I am trying to #Inject it with Dagger, but the mocking wasn't successful:
class SettingsRepositoryTest {
private lateinit var settingsDao: SettingsDao
#Mock
private lateinit var mockLifecycleOwner: LifecycleOwner
#Before
fun createDb(){
MockitoAnnotations.initMocks(LifecycleOwner::class.java)
val appContext = InstrumentationRegistry.getTargetContext()
val db = Room.inMemoryDatabaseBuilder(appContext, CurrencyConverterDb::class.java).allowMainThreadQueries().build()
settingsDao = db.settingsDao()
}
#Test
fun testSettingsMap() {
val repo = SettingsRepository(settingsDao, mockLifecycleOwner) // throws the exception here
}
}
The exception:
kotlin.UninitializedPropertyAccessException: lateinit property mockLifecycleOwner has not been initialized
at com.helmet91.currencyconverter.repositories.SettingsRepositoryTestInst.testSettingsMap(SettingsRepositoryTestInst.kt:46)
2nd attempt: use Roboelectric to create an AppCompatActivity, which is in fact a LifecycleOwner.
It is not an instrumented test, because Roboelectric doesn't work in the androidTest environment.
The Activity still has to be mocked, however, it throws a NullPointerException. The only way I can think of, is to go through this Exception stack, and mock everything in it, if it's even possible at all. But that sounds insane to me. There has to be a better solution.
class SettingsRepositoryTest {
private lateinit var settingsDao: SettingsDao
private lateinit var activity: AppCompatActivity
#Before
fun createDb(){
val built = Robolectric.buildActivity(MainActivity::class.java) // throws the exception here
val created = built.create()
val controller = created.start()
activity = controller.get() as AppCompatActivity
val db = Room.inMemoryDatabaseBuilder(activity, CurrencyConverterDb::class.java).allowMainThreadQueries().build()
settingsDao = db.settingsDao()
}
#Test
fun testSettingsMap() {
val repo = SettingsRepository(settingsDao, activity)
val settingsMap = repo.getSettings()
val settingsEntity = Settings(1, "show_flags", "1", "bool")
settingsDao.insert(settingsEntity)
assertTrue(settingsMap.show_flags)
}
}
The exception:
java.lang.NullPointerException
at org.robolectric.internal.bytecode.ShadowImpl.extract(ShadowImpl.java:17)
at org.robolectric.shadow.api.Shadow.extract(Shadow.java:25)
at org.robolectric.Shadows.shadowOf(Shadows.java:1215)
at org.robolectric.shadows.CoreShadowsAdapter.getMainLooper(CoreShadowsAdapter.java:23)
at org.robolectric.android.controller.ComponentController.<init>(ComponentController.java:29)
at org.robolectric.android.controller.ComponentController.<init>(ComponentController.java:21)
at org.robolectric.android.controller.ActivityController.<init>(ActivityController.java:33)
at org.robolectric.android.controller.ActivityController.of(ActivityController.java:25)
at org.robolectric.Robolectric.buildActivity(Robolectric.java:97)
at org.robolectric.Robolectric.buildActivity(Robolectric.java:93)
at com.helmet91.currencyconverter.repositories.SettingsRepositoryTest.createDb(SettingsRepositoryTest.kt:29)
Is it really impossible to test anything, that involves Lifecycle components?

Related

Getting data from a private constructor class in to main activity (Android in app purchases)

I am trying to implement in app purchases with the billing library and I just have one thing that I am failing to implement for a few days now.
To begin with, my billing class is defined as so:
class BillingHelper private constructor(
context: Context,
private val defaultScope: CoroutineScope,
knownInAppSKUs: Array<String>?
) {
For now, I am simply trying to obtain a single in app purchase details, which I have done successfully, but I can't manage to get it in to my main activity. The method that I use (after connecting to google play billing services) to query for the product details is as follows:
private fun queryProducts() {
val productList =
listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("remove_ads")
.setProductType(BillingClient.ProductType.INAPP)
.build()
)
val params = QueryProductDetailsParams.newBuilder().setProductList(productList)
billingClient.queryProductDetailsAsync(params.build()) {
billingResult,
productDetailsList ->
// Process the result
}
}
I would like to get the data in the async query to main activity. I am aware the implementation is most likely to be done with mutable state or shared flows, but I am fairly new to Kotlin so I failed to implement them. Specifically I failed to successfully collect them in the main activity. Could I perhaps turn this class in to a ViewModel and use LiveData?
Any tips or pointers on how to go about with implementing this are greatly appreciated!
Solved! Implemented my class as so:
class BillingHelper private constructor() : ViewModel() {
Used the following live data:
private val productData = MutableLiveData<ProductDetails>()
Sent the values back to main:
productData.postValue(productDetailsList[0])
...
fun getLiveData(): MutableLiveData<ProductDetails> {
return productData
}
And finally obtain the values back in the main activity in the onCreate method as such:
val liveDataObserver: Observer<ProductDetails?> = Observer<ProductDetails?> {
Log.i("billing", it.name)
}
bill.getLiveData().observe(this, liveDataObserver)
Also I removed context from constructors and instead now am getting context from application. Hope this is useful for someone!

Android MVVM testing with Mockito, mock repository.observeValue() has problem

I'm trying to test MVVM with Mockito.
The architecture of MVVM is similar to Android architecture blueprints.
We observe data from repository as LiveData.
And try to test observed value like below code.
class SplashViewModel(
private val appRepository: AppRepository
) {
val appInfo: LiveData<AppInfo> = appRepository.observeAppInfo()
}
#Test
fun getAppInfo() {
`when`(appRepository.observeAppInfo())
.thenReturn(appInfoData)
assertEquals(appInfoData, viewModel.appInfo.getOrAwaitValue())
}
The crucial point is viewModel.appInfo returns null, despite of I used mockito.
The problem
The creation of ViewModel is faster than using mockito.
So appInfo property is initialized with null, cause it dosen't know what observeAppInfo() is.
First solution
At first, I just trying to solve this problem with custom getter. Like this.
class SplashViewModel(
private val appRepository: AppRepository
) {
val appInfo: LiveData<AppInfo> get() = appRepository.observeAppInfo()
}
Now every time I access to appInfo they just re evaluate the data.
But is has it's own problem.
In this situation appVersion is getting error.
val appVersion: LiveData<String> = appInfo.map {
...
}
So every transformation LiveData(Like Transformations, MediatorLiveData) must use custom getter too.
And I felt it's not a great solution.
How do you think of it?
You could use #BeforeClass to make sure your initialisation is run before the tests.
#BeforeClass
fun setup(){
appInfo = appRepository.observeAppInfo()
}
See more

Creating Unit Tests for ViewModels

I have recently completed this (links below) codelabs tutorial which walks through how to implement Room with LiveData and Databinding in Kotlin.
https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/
https://github.com/googlecodelabs/android-room-with-a-view/tree/kotlin
Following on from this, I want to write some tests around the ViewModel, however, the GitHub repository where the code is stored does not contain any (it has a few tests around the DAO, not what I am interested in for now).
The ViewModel I am trying to test looks like this:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}
My test class for the ViewModel looks like this:
#RunWith(JUnit4::class)
class WordViewModelTest {
private val mockedApplication = mock<Application>()
#Test
fun checkAllWordsIsEmpty() {
val vm = WordViewModel(mockedApplication)
assertEquals(vm.allWords, listOf<String>())
}
}
I get an error saying java.lang.IllegalArgumentException: Cannot provide null context for the database. This error then points to this line in the WordViewModel: val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao(). To get this to not crash, I believe I need to mock a lot of what is in the ViewModel, which I am fine with.
I would like to be able to run the test above and in the future, I would also like to mock return a list of data when repository.allWords is called. However, I am not sure how to do this. So my question is, how can I mock the following lines from WordViewModel to allow me to do this?
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords

How to write a use case that retrieves data from Android framework with Context

I am migrating an application to MVVM and clean architecture, and I am missing one part of the puzzle.
The problem domain:
List all applications on device and display them in the Fragment / Activity
A device app is represented by its package name:
data class DeviceApp(val packageName: String)
This is how the device apps are listed:
private fun listAllApplications(context: Context): List<DeviceApp> {
val ans = mutableListOf<DeviceApp>()
val packageManager: PackageManager = context.applicationContext.packageManager
val packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
for (applicationInfo in packages) {
val packageName = applicationInfo.packageName
ans.add(DeviceApp(packageName))
}
return ans
}
As I understand, calling listAllApplications() should be done in a UseCase inside the 'Domain Layer', which is called by a ViewModel.
However listAllApplications receives a Context, and the Domain Layer should be plain code only.
In clean architecture / MVVM, in which layer should I put listAllApplications(context)?
And more generally, how should the ViewModel interact with Android framework APIs that require Context (location, etc.)?
Domain Layer should be plain code only.
That's correct!, but in my opinion it's partially correct. Now considering your scenario you need context at domain level. You shouldn't have context at domain level but in your need you should either choose other architecture pattern or consider it as exceptional case that you're doing this.
Considering you're using context at domain, you should always use applicationContext in spite of activity context, because earlier persists through out process.
How should the ViewModel interact with android framework APIs that require Context (location, etc.)?
Whenever you need Context at ViewModel either you can provide it from UI as method parameter (I.e. viewModel.getLocation(context)) or else use AndroidViewModel as your parent class for ViewModel (it provides getApplication() public method to access context through out ViewModel).
All I would like to point you out is that make sure you don't accidentally hold any View/Context globally inside ViewModel/Domain Layer, because it can make catastrophe like memory leaking or crashes at worse.
You can solve this problem very cleanly with dependency-injection. If you aren't already using DI, you probably want to be, as it will greatly simplify your clean-architecture endeavours.
Here's how I'd do this with Koin for DI.
First, convert your usecase from a function to a class. This allows for constructor injection:
class ListAllApplications(private val context: Context) {
...
}
You now have a reference to context inside your usecase. Great! We'll deal with actually providing the value of context in a moment.
Now you're thinking... but aren't usecases meant to use reusable functions? What's the guy on about with usecases being classes?
We can leverage the miracle that is operator funs to help us here.
class ListAllApplications(private val context: Context) {
operator fun invoke(): List<DeviceApp> {
val ans = mutableListOf<DeviceApp>()
val packageManager: PackageManager = context.applicationContext.packageManager
val packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
for (applicationInfo in packages) {
val packageName = applicationInfo.packageName
ans.add(DeviceApp(packageName))
}
return ans
}
}
invoke is a special function which allows an instance of a class to be invoked as if it were a function. It effectively transforms our class into a function with an injectable constructor 🤯
And this allows us to continue to invoke our usecase in the ViewModel with the standard function invocation syntax:
class MyViewModel(private val listAllApplications: ListAllApplications): ViewModel {
init {
val res = listAllApplications()
}
}
Notice that our ListAllApplications usecase is also being injected into the constructor of MyViewModel, meaning that the ViewModel remains entirely unaware of Context.
The final piece of the puzzle is wiring all this injection together with Koin:
object KoinModule {
private val useCases = module {
single { ListAllApplications(androidContext()) }
}
private val viewModels = module {
viewModel { MyViewModel(get()) }
}
}
Don't worry if you've never used Koin before, other DI libraries will let you do similar things. The key is that your ListAllApplications instance is being constructed by Koin, which provides an instance of Context with androidContext(). Your MyViewModel instance is also being constructed by Koin, which provides the instance of ListAllApplications with get().
Finally you inject MyViewModel into the Activity/Fragment which uses it. With Koin that's as simple as:
class MyFragment : Fragment {
private val viewModel: MyViewModel by viewModel()
}
Et Voilà!

Mockito testing in MVP kotlin - However, there was exactly 1 interaction with this mock exception

I have developed an application with Kotlin and MVP architecture.I want to do unit testing using Mockito but unable to achieve it.I'm getting "However, there was exactly 1 interaction with this mock exception".
In my application i have used 5 different classes.
View - Interface
Intractor - Interface
IntractorImpl - Class
Presenter - Interfacce
PresenterImpl - Class
View:- It contain all methods required for to communicate activity/fragment.
Intractor:- It contain the result interface methods.
IntractorImpl :- It contain database/server logic, it will implemented from Intractor
Presenter :- It contain methods to access the IntractorImpl class
PresenterImpl :- It is implemented from Presenter and Intractor result methods
This architecture which i followed for implementing, now to implement the Mockito i'm getting confusion and as per the articles i found in google if i try to implement i'm getting "However, there was exactly 1 interaction with this mock exception"
For mockito i have used following imports
//Mockito
testImplementation "org.mockito:mockito-core:2.24.5"
testImplementation "org.mockito:mockito-inline:2.24.5"
Please if anyone knows anything,help me out. First time i'm using Mokito.
If anyone need more information i can post it. Following is my code
class EventTest {
#Mock
val homeActivity = HomeActivity()
lateinit var eventView: HomeView
lateinit var eventInteractorImpl: HomeInteractorImpl
lateinit var eventPresenter: HomePresenter
lateinit var eventInteractor: HomeInteractor.OnEventsReceivedListener
#Before
fun setUpEventPresenter() {
eventView = mock(HomeView::class.java)
eventInteractorImpl = mock(HomeInteractorImpl::class.java)
eventPresenter = HomePresenterImpl(eventView, eventInteractorImpl)
eventInteractor = mock(HomeInteractor.OnEventsReceivedListener::class.java)
}
#Test
fun startEventTest() {
val testEvents = Event(
"1",
"Test",
"Next week we have the vacation planing in Vizag",
"This is for test",
"1",
"01-10-2019",
"26-09-2019",
"8.00",
"10.47",
"",
true
)
val events: ArrayList<Event> = ArrayList<Event>()
events.add(testEvents)
eventPresenter.getEventsList(homeActivity)
verify(eventView).showProgress()
verify(eventView).showEvents(events)
verify(eventView).hideProgress()
}
}
If i put verify(eventView).showProgress() no exception is coming.
override fun getEventsList(context: Context) {
homeView.showProgress()
homeInteractor.getEvents(context, this)
}
In getEvents() method we are calling server/local database, for Server we are using Retrofit.
According to your code and explanation you gave in comments, the error message you're getting is correct because you're invoking mock interactor which cannot invoke other view functions.
Therefore, only thing you should verify in startEventTest() is verify(eventView).showProgress() and verify(eventInteractorImpl).getEvents(any(), any()):
#Test
fun startEventTest() {
val testEvents = Event(
"1",
"Test",
"Next week we have the vacation planing in Vizag",
"This is for test",
"1",
"01-10-2019",
"26-09-2019",
"8.00",
"10.47",
"",
true
)
val events: ArrayList<Event> = ArrayList<Event>()
events.add(testEvents)
eventPresenter.getEventsList(homeActivity)
verify(eventView).showProgress()
verify(eventInteractorImpl).getEvents(any(), any())
}
If you still want to test verify(eventView).showEvents(events) and verify(eventView).hideProgress(), instead of mocking interactor, you should create instance of it.

Categories

Resources