Unit Test ViewModels and Helper/Manager classes - android

I have a question related to Unit testing in android.
The app is written in Kotlin, MVVM architecture, Dagger-Hilt, Room, etc…
I have written all the tests for Room DAOs, according to official docs. I have created fake repositories so I can test some Managers/Helpers (these classes encapsulate logic I have to reuse in many ViewModels) that handle business logic. Now I need to test ViewModels which have these Managers/Helpers as dependencies.
I don’t want to fall into trap of re-testing the same code all over again, the question is how to test ViewModels?
Should I only test the parameters that are passed to functions in these Managers/Helpers, and write assertions for that, or what to do?
Thanks in advance!

That is what mocks are for. You should test only logic of ViewModel ignoring logic inside Managers/Helpers. Everything else should be mocked/faked:
#Test
fun `invalid login`() {
runBlocking {
//prepare
val validator = mock<Validator> {
on { errorFor(any()) }
.thenReturn("Something wrong")
}
val authNavigator = spy(loginNavigator)
val tracker = spy(ActionsTracker(mock()))
val trackers = Trackers(tracker, ViewsTracker(mock()), ClicksTracker(mock()), ImpressionsTracker(mock()))
assertViewModel(
validation = AuthViewModel.LoginValidation(validator, validator),
authNavigator = authNavigator,
trackers = trackers
) {
email.set("test")
password.set("test")
//assert
assertNull(emailError.get())
assertNull(passwordError.get())
//act
login()
//assert
assertNotNull(emailError.get())
assertNotNull(passwordError.get())
verify(authNavigator, never()).navigateToHome()
verify(tracker, never()).userPropertiesChanged(any(), any())
}
}
}

Related

There are multiple DataStores active for the same file in HiltAndroidTest

I just added DataStore to our codebase. After that, I found that all sequential UI tests are failing - the first one in a test case pass but next fails with There are multiple DataStores active for the same file.
I provide a data store instance using Hilt
#InstallIn(SingletonComponent::class)
#Module
internal object DataStoreModule {
#Singleton
#Provides
internal fun provideConfigurationDataStore(
#ApplicationContext context: Context,
configurationLocalSerializer: ClientConfigurationLocalSerializer
): DataStore<ClientConfigurationLocal> = DataStoreFactory.create(
serializer = configurationLocalSerializer,
produceFile = { context.dataStoreFile("configuration.pb") }
)
}
I guess this is happening because In a Hilt test, the singleton component’s lifetime is scoped to the lifetime of a test case rather than the lifetime of the Application.
Any ideas on how to workaround this?
I had the same issue.
One solution I tried but which didn't work (correctly) is to make sure the tests, once done, remove the dataStore files (the whole folder) and close the scope (the overridden scope that you manage in a "manager" class), like so:
https://github.com/wwt/testing-android-datastore/blob/main/app/src/androidTest/java/com/wwt/sharedprefs/DataStoreTest.kt
I had this in a finished() block of a TestWatcher used for these UI tests. For some reason, this was not enough so I ended up not looking deeper into why.
Instead I just used a simpler solution: the UI tests would use their own Dagger component, which has its own StorageModule module, which provides its own IStorage implementation, which for UI tests is backed just by an in-memory map, whereas on a production Dagger module would back it up via a DataStore:
interface IStorage {
suspend fun retrieve(key: String): String?
suspend fun store(key: String, data: String)
suspend fun remove(key: String)
suspend fun clear()
I prefer this approach in my case as I don't need to test the actual disk-persistance of this storage in UI tests, but if I had needed it, I'd investigate further into how to reliably ensure the datastore folder and scope are cleaned up before/after each UI test.
I was having the same issues and I came out with a workaround. I append a random number to the file name of the preferences for each test case and I just delete the whole datastore file afterward.
HiltTestModule
#Module
#TestInstallIn(
components = [SingletonComponent::class],
replaces = [LocalModule::class, RemoteModule::class]
)
object TestAppModule {
#Singleton
#Provides
fun provideFakePreferences(
#ApplicationContext context: Context,
scope: CoroutineScope
): DataStore<Preferences> {
val random = Random.nextInt() // generating here
return PreferenceDataStoreFactory
.create(
scope = scope,
produceFile = {
// creating a new file for every test case and finally
// deleting them all
context.preferencesDataStoreFile("test_pref_file-$random")
}
)
}
}
#After function
#After
fun teardown() {
File(context.filesDir, "datastore").deleteRecursively()
}
I'd suggest for more control + better unit-test properties (ie. no IO, fast, isolated) oblakr24's answer is a good clean way to do this; abstract away the thing that you don't own that has behavior undesirable in tests.
However, there's also the possibility these tests are more like end-to-end / feature tests, so you want them to be as "real" as possible, fewer test doubles, maybe just faking a back-end but otherwise testing your whole app integrated. If so, you ought to use the provided property delegate that helps to ensure a singleton, and declare it top-level, outside a class, as per the docs. That way the property delegate will only get created once within the class-loader, and if you reference it from somewhere else (eg. in your DI graph) that will get torn down and recreated for each test, it won't matter; the property delegate will ensure the same instance is used.
A more general solution, not limited to Hilt, would be to mock Context.dataStoreFile() function with mockk to return a random file name.
I like this approach as it doesn't require any changes on the production code.
Example of TestWatcher:
class CleanDataStoreTestRule : TestWatcher() {
override fun starting(description: Description) {
replaceDataStoreNamesWithRandomUuids()
super.starting(description)
}
override fun finished(description: Description) {
super.finished(description)
removeDataStoreFiles()
}
private fun replaceDataStoreNamesWithRandomUuids() {
mockkStatic("androidx.datastore.DataStoreFile")
val contextSlot = slot<Context>()
every {
capture(contextSlot).dataStoreFile(any())
} answers {
File(
contextSlot.captured.filesDir,
"datastore/${UUID.randomUUID()}",
)
}
}
private fun removeDataStoreFiles() {
InstrumentationRegistry.getInstrumentation().targetContext.run {
File(filesDir, "datastore").deleteRecursively()
}
}
}
and then use it in tests:
class SomeTest {
#get:Rule
val cleanDataStoreTestRule = CleanDataStoreTestRule()
...
}
The solution assumes that you use Context.dataStoreFile() and that the file name does not matter. IMO the assumptions are reasonable in most cases.

JUnit 5 - Parameterized Nested Tests

Expected
Creating a nested test within a parameterized test in JUnit5.
There are many conditions for the Android ViewModel using param. tests. I want to organize the tests within the param. test to improve output readability.
#ExtendWith(InstantExecutorExtension::class)
class ContentViewModelTest {
private fun `FeedLoad`() = Stream.of(
FeedLoadTest(isRealtime = false, feedType = MAIN, timeframe = DAY, lceState = LOADING),
FeedLoadTest(isRealtime = false, feedType = MAIN, timeframe = DAY, lceState = CONTENT))
#ParameterizedTest
#MethodSource("FeedLoad")
fun `Feed Load`(test: FeedLoadTest) {
#Nested
class FeedLoadNestedTest {
#Test
fun `all fields are included`() {
assertThat(4).isEqualTo(2 + 2)
}
#Test
fun `limit parameter`() {
assertThat(4).isEqualTo(3 + 2)
}
}
...
}
data class FeedLoadTest(val isRealtime: Boolean, val feedType: FeedType,
val timeframe: Timeframe, val lceState: LCE_STATE)
}
Observed
The normal parameterized assertions [not depicted] work as expected. The nested FeedLoadNestedTest does not run within the Stream of parameterized FeedLoad tests.
#Sam Brannen, thanks for the feedback!
Sam has indicated on GitHub, #Nested annotation on local classes will not be a viable option.
We have no plans to support #Nested on local classes defined within the scope of a method (function in Kotlin).
Solution
Implement multiple parameterized tests that pass in the same stream.
This will allow for assertions and logic to be organized into separate parameterized functions while testing the same data passed in via the stream.
#ParameterizedTest
#MethodSource("FeedLoadStream")
fun `Feed Load Part One`(test: FeedLoadTest) {
...
}
#ParameterizedTest
#MethodSource("FeedLoadStream")
fun `Feed Load Part Two`(test: FeedLoadTest) {
...
}
#ParameterizedTest
#MethodSource("FeedLoadStream")
fun `Feed Load Part Three`(test: FeedLoadTest) {
...
}

Deadlock when using runBlocking coroutine for retrieve Firebase's AuthUI.signOut() result

I currently learning how to use Kotlin coroutines for Android applications. And one of the things that fascinates me a lot is a possibility to treat asynchronous procedures synchronously.
So, my toy application based on MVVM architecture pattern and using Firebase to implement sign in/out functionality. And there is a SignOut use case:
class FirebaseSignOut #Inject constructor(private val firebaseAuthUi: AuthUI) : SignOut {
private lateinit var signOutResult: SignOutResult
override fun execute(context: Context): SignOutResult {
runBlocking {
signOutResult = getSignOutResult(context)
}
return signOutResult
}
private suspend fun getSignOutResult(context: Context): SignOutResult {
return suspendCoroutine { continuation ->
firebaseAuthUi.signOut(context)
.addOnSuccessListener {
continuation.resume(SignOutSuccess)
}
.addOnFailureListener {
continuation.resume(SignOutError)
}
.addOnCanceledListener {
continuation.resume(SignOutCancel)
}
}
}
}
The problem
is that blocking started with runBlocking is never unlocks.
I have figured out that coroutines logic itself is not a problem by replacing getSignOutResult() by mock, for example:
private suspend fun getSignOutResult(context: Context): SignOutResult {
delay(500)
return SignOutError
}
Then I was able to track the blocking til this part of Firebase's AuthUI:
return Tasks.whenAll(
signOutIdps(context),
maybeDisableAutoSignIn
).continueWith(new Continuation<Void, Void>() {
https://github.com/firebase/FirebaseUI-Android/blob/master/auth/src/main/java/com/firebase/ui/auth/AuthUI.java#L325
private Task<Void> signOutIdps(#NonNull Context context) {
...
return GoogleSignIn.getClient(context, GoogleSignInOptions.DEFAULT_SIGN_IN).signOut();
}
https://github.com/firebase/FirebaseUI-Android/blob/master/auth/src/main/java/com/firebase/ui/auth/AuthUI.java#L403
and figure out that maybeDisableAutoSignIn Task is never starts. So, deadlock happening somewhere inside GoogleSignIn's signOut(), which is unable to trace.
Question
Does anybody have an idea why such a deadlock can possibly happen? And any suggestion how to solve the problem is greatly welcome.
Configuration details
I use currently latest versions of Kotlin and Firebase libraries
firebaseCoreVersion = '16.0.4'
firebaseUiVersion = '4.1.0'
kotlinVersion = '1.3.0'
kotlinCoroutinesVersion = '1.0.0'
AuthUI settled to use only Google sign in.
! PLEASE NOTE that I know I can make signOutResult variable observable. But in this case execute will not be able to return a result. And I really want to do it because such implementation is easy mockable for tests. And the fact that runBlocking blocks main thread for a sign out is acceptable in my case - it makes sense that UI is unavailable during the procedure, since user interface changes significantly depending on authentication state.

Testing LiveData Transformations?

I've built a Splash Screen using Android Architecture Components and Reactive approach.
I return from Preferences LiveData object fun isFirstLaunchLD(): SharedPreferencesLiveData<Boolean>.
I have ViewModel that passes LiveData to the view and updates Preferences
val isFirstLaunch = Transformations.map(preferences.isFirstLaunchLD()) { isFirstLaunch ->
if (isFirstLaunch) {
preferences.isFirstLaunch = false
}
isFirstLaunch
}
In my Fragment, I observe LiveData from ViewModel
viewModel.isFirstLaunch.observe(this, Observer { isFirstLaunch ->
if (isFirstLaunch) {
animationView.playAnimation()
} else {
navigateNext()
}
})
I would like to test my ViewModel now to see if isFirstLaunch is updated properly. How can I test it? Have I separated all layers correctly? What kind of tests would you write on this sample code?
Have I separated all layers correctly?
The layers seem reasonably separated. The logic is in the ViewModel and you're not referring to storing Android Views/Fragments/Activities in the ViewModel.
What kind of tests would you write on this sample code?
When testing your ViewModel you can write instrumentation or pure unit tests on this code. For unit testing, you might need to figure out how to make a test double for preferences, so that you can focus on the isFirstLaunch/map behavior. An easy way to do that is passing a fake preference test double into the ViewModel.
How can I test it?
I wrote a little blurb on testing LiveData Transformations, read on!
Testing LiveData Transformations
Tl;DR You can test LiveData transformation, you just need to make sure the result LiveData of the Transformation is observed.
Fact 1: LiveData doesn't emit data if it's not observed. LiveData's "lifecycle awareness" is all about avoiding extra work. LiveData knows what lifecycle state it's observers (usually Activities/Fragments) are in. This allows LiveData to know if it's being observed by anything actually on-screen. If LiveData aren't observed or if their observers are off-screen, the observers are not triggered (an observer's onChanged method isn't called). This is useful because it keeps you from doing extra work "updating/displaying" an off-screen Fragment, for example.
Fact 2: LiveData generated by Transformations must be observed for the transformation to trigger. For Transformation to be triggered, the result LiveData (in this case, isFirstLaunch) must be observed. Again, without observation, the LiveData observers aren't triggered, and neither are the transformations.
When you're unit testing a ViewModel, you shouldn't have or need access to a Fragment/Activity. If you can't set up an observer the normal way, how do you unit test?
Fact 3: In your tests, you don't need a LifecycleOwner to observe LiveData, you can use observeForever You do not need a lifecycle observer to be able to test LiveData. This is confusing because generally outside of tests (ie in your production code), you'll use a LifecycleObserver like an Activity or Fragment.
In tests you can use the LiveData method observeForever() to observer without a lifecycle owner. This observer is "always" observing and doesn't have a concept of on/off screen since there's no LifecycleOwner. You must therefore manually remove the observer using removeObserver(observer).
Putting this all together, you can use observeForever to test your Transformations code:
class ViewModelTest {
// Executes each task synchronously using Architecture Components.
// For tests and required for LiveData to function deterministically!
#get:Rule
val rule = InstantTaskExecutorRule()
#Test
fun isFirstLaunchTest() {
// Create observer - no need for it to do anything!
val observer = Observer<Boolean> {}
try {
// Sets up the state you're testing for in the VM
// This affects the INPUT LiveData of the transformation
viewModel.someMethodThatAffectsFirstLaunchLiveData()
// Observe the OUTPUT LiveData forever
// Even though the observer itself doesn't do anything
// it ensures any map functions needed to calculate
// isFirstLaunch will be run.
viewModel.isFirstLaunch.observeForever(observer)
assertEquals(viewModel.isFirstLaunch.value, true)
} finally {
// Whatever happens, don't forget to remove the observer!
viewModel.isFirstLaunch.removeObserver(observer)
}
}
}
A few notes:
You need to use InstantTaskExecutorRule() to get your LiveData updates to execute synchronously. You'll need the androidx.arch.core:core-testing:<current-version> to use this rule.
While you'll often see observeForever in test code, it also sometimes makes its way into production code. Just keep in mind that when you're using observeForever in production code, you lose the benefits of lifecycle awareness. You must also make sure not to forget to remove the observer!
Finally, if you're writing a lot of these tests, the try, observe-catch-remove-code can get tedious. If you're using Kotlin, you can make an extension function that will simplify the code and avoid the possibility of forgetting to remove the observer. There are two options:
Option 1
/**
* Observes a [LiveData] until the `block` is done executing.
*/
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
val observer = Observer<T> { }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}
Which would make the test look like:
class ViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#Test
fun isFirstLaunchTest() {
viewModel.someMethodThatAffectsFirstLaunchLiveData()
// observeForTesting using the OUTPUT livedata
viewModel.isFirstLaunch.observeForTesting {
assertEquals(viewModel.isFirstLaunch.value, true)
}
}
}
Option 2
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this#getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
#Suppress("UNCHECKED_CAST")
return data as T
}
Which would make the test look like:
class ViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#Test
fun isFirstLaunchTest() {
viewModel.someMethodThatAffectsFirstLaunchLiveData()
// getOrAwaitValue using the OUTPUT livedata
assertEquals(viewModel.isFirstLaunch.getOrAwaitValue(), true)
}
}
These options were both taken from the reactive branch of Architecture Blueprints.
It depends on what your SharedPreferencesLiveData does.
If the SharedPreferencesLiveData contains Android specific classes, you won't be able to test this correctly because JUnit won't have access to the Android specific classes.
The other issue is that to be able to observe LiveData, you need some kind of Lifecycle owner. (The this in the original post code.)
In the Unit test, the 'this' can simply be replaced with something like the following:
private fun lifecycle(): Lifecycle {
val lifecycle = LifecycleRegistry(Mockito.mock(LifecycleOwner::class.java))
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
return lifecycle
}
And then used in the following way:
#RunWith(MockitoJUnitRunner::class)
class ViewModelTest {
#Rule
#JvmField
val liveDataImmediateRule = InstantTaskExecutorRule()
#Test
fun viewModelShouldLoadAttributeForConsent() {
var isLaunchedEvent: Boolean = False
// Pseudo code - Create ViewModel
viewModel.isFirstLaunch.observe(lifecycle(), Observer { isLaunchedEvent = it } )
assertEquals(true, isLaunchedEvent)
}
private fun lifecycle(): Lifecycle {
val lifecycle = LifecycleRegistry(Mockito.mock(LifecycleOwner::class.java))
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
return lifecycle
}
}
Note: You have to have the Rule present so that the LiveData executes instantly instead of whenever it wants to.

Unit testing coroutines on UI thread

I'm using coroutines to do an asynchronous call on pull to refresh like so:
class DataFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {
// other functions here
override fun onRefresh() {
loadDataAsync()
}
private fun loadDataAsync() = async(UI) {
swipeRefreshLayout?.isRefreshing = true
progressLayout?.showContent()
val data = async(CommonPool) {
service?.getData() // suspending function
}.await()
when {
data == null -> showError()
data.isEmpty() -> progressLayout?.showEmpty(null, parentActivity?.getString(R.string.no_data), null)
else -> {
dataAdapter?.updateData(data)
dataAdapter?.notifyDataSetChanged()
progressLayout?.showContent()
}
}
swipeRefreshLayout?.isRefreshing = false
}
}
Everything here works fine when I actually put it on a device. My error, empty, and data states are all handled well and the performance is good. However, I'm also trying to unit test it with Spek. My Spek test looks like this:
#RunWith(JUnitPlatform::class)
class DataFragmentTest : Spek({
describe("The DataFragment") {
var uut: DataFragment? = null
beforeEachTest {
uut = DataFragment()
}
// test other functions
describe("when onRefresh") {
beforeEachTest {
uut?.swipeRefreshLayout = mock()
uut?.onRefresh()
}
it("sets swipeRefreshLayout.isRefreshing to true") {
verify(uut?.swipeRefreshLayout)?.isRefreshing = true // says no interaction with mock
}
}
}
}
The test is failing because it says that there was no interaction with the uut?.swipeRefreshLayout mock. After some experimenting, it seems this is because I'm using the UI context via async(UI). If I make it just be a regular async, I can get the test to pass but then the app crashes because I'm modifying views outside of the UI thread.
Any ideas why this might be occurring? Also, if anyone has any better suggestions for doing this which will make it more testable, I'm all ears.
Thanks.
EDIT: Forgot to mention that I also tried wrapping the verify and the uut?.onRefresh() in a runBlocking, but I still had no success.
If you want to make things clean and consider using MVP architecture in the future you should understand that CourutineContext is external dependency, that should be injected via DI, or passed to your presenter. More details on topic.
The answer for your question is simple, you should use only Unconfined CourutineContext for your tests. (more)
To make things simple create an object e.g. Injection with:
package com.example
object Injection {
val uiContext : CourutineContext = UI
val bgContext : CourutineContext = CommonPool
}
and in test package create absolutely the same object but change to:
package com.example
object Injection {
val uiContext : CourutineContext = Unconfined
val bgContext : CourutineContext = Unconfined
}
and inside your class it will be something like:
val data = async(Injection.bgContext) {service?.getData()}.await()

Categories

Resources