Android: unit testing of LiveData and Flow - kotlin

I'm trying to write unit testing for my ViewModel but I don't know how to deal with LiveData functions.
Specifically I'm not able to validate all the values that receive the LiveData Observer.
Regarding I have a Flow Use case that emit values and then is pased as a LiveData, what is the best approach to test operation function?
In the code below you can find that I'm only able to read the value "endLoading", but I want to check all the values: "startLoading", "Hello Dummy $input", "endLoading"
MainViewModel.kt
class MainViewModel(val useCase: DummyUseCase = DummyUseCase()): ViewModel() {
fun operation(value: Int): LiveData<String> = useCase.invoke(value)
.transform { response ->
emit(response)
}.onStart {
emit("startLoading")
}.catch {
emit("ERROR")
}.onCompletion {
emit("endLoading")
}.asLiveData(viewModelScope.coroutineContext)
}
MainViewModelTest.kt
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
#ExperimentalCoroutinesApi
class MainViewModelTest {
//region Setup
#get:Rule
val rule = InstantTaskExecutorRule()
private val testDispatcher = TestCoroutineDispatcher()
#MockK private lateinit var stateObserver: Observer<String>
#MockK private lateinit var useCase: DummyUseCase
private lateinit var viewModel: MainViewModel
#Before
fun setup() {
MockKAnnotations.init(this, relaxUnitFun = true)
Dispatchers.setMain(testDispatcher)
viewModel = MainViewModel(useCase)
}
#After
fun teardown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
//endregion
#Test // AAA testing
fun `when my flow succeeds, return a state String`() {
runBlocking {
//Arrange
val input = 10
coEvery { useCase.invoke(input) }.returns(flow {
emit("Hello Dummy $input")
})
//Act
val actual = viewModel.operation(input).apply {
observeForever(stateObserver)
}
//Assert
// I want to assert here every value received in the observer of the "actual" LiveData
// How? :(
assertNotNull(actual.value) // is always "endLoading"
}
}
}

You can test the LiveData using a custom Observer<T> implementation. Create an observer which records all emmited values and lets you assert against the history.
The Observer which records the values may look like this:
class TestableObserver<T> : Observer<T> {
private val history: MutableList<T> = mutableListOf()
override fun onChanged(value: T) {
history.add(value)
}
fun assertAllEmitted(values: List<T>) {
assertEquals(values.count(), history.count())
history.forEachIndexed { index, t ->
assertEquals(values[index], t)
}
}
}
You can assert if all given values were emitted by the LiveData using the assertAllEmitted(...) function.
The test function will use an instance of the TestableObserver class instead of a mocked one:
#Test // AAA testing
fun `when my flow succeeds, return a state String`() {
runBlocking {
//Arrange
val stateObserver = TestableObserver<String>()
val input = 10
coEvery { useCase.invoke(input) }.returns(flow {
emit("Hello Dummy $input")
})
//Act
val actual = viewModel.operation(input).apply {
observeForever(stateObserver)
}
//Assert
stateObserver.assertAllEmitted(
listOf(
"startLoading",
"Hello Dummy 10",
"endLoading"
)
)
}
}
Asserting the history of LiveData may be possible using mocking frameworks and assertion frameworks however, I think implementing this testable observer is more readable.

Related

Type adapter warning when registering java.util.List

When trying to register a type adapter, where list is java.util.List
GsonBuilder().registerTypeAdapter(object : TypeToken<List<MyObject>>() {}.type, MyObjectsTypeAdapter())
I get the following warning:
This class shouldn't be used in Kotlin. Use kotlin.collections.List or
kotlin.collections
My type adapter uses a kotlin.collections.list.
class MyObjectsTypeAdapter: TypeAdapter<List<MyObject>>() {
...
}
However if I don't use java.util.List in my type adapter, gson does not match the type correctly.
Am I doing something else wrong when registering my type adapter?
Below is small demo showing Retrofit usage with Gson. A few important points:
To use Gson as converter you have to add the com.squareup.retrofit2:converter-gson dependency and add the converter factory with addConverterFactory(GsonConverterFactory.create()); see Retrofit documentation
The custom TypeAdapterFactory in the example below is not needed; it just shows how you could customize the Gson instance
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import retrofit2.*
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
data class MyObject(
val message: String
)
interface DemoService {
#GET("demo")
fun demo(): Call<List<MyObject>>
}
fun main() {
val port = 8080
// Start a demo server returning the JSON response
embeddedServer(Netty, port) {
routing {
get("/demo") {
call.respondText("[{\"message\":\"Hello from server\"}]")
}
}
}.start(wait = false)
val gson = GsonBuilder()
.registerTypeAdapterFactory(object: TypeAdapterFactory {
override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType != MyObject::class.java) {
return null
}
#Suppress("UNCHECKED_CAST") // safe due to type check at beginning
val delegate = gson.getDelegateAdapter(this, type) as TypeAdapter<MyObject>
val adapter = object: TypeAdapter<MyObject>() {
override fun write(writer: JsonWriter, value: MyObject?) {
return delegate.write(writer, value)
}
override fun read(reader: JsonReader): MyObject? {
val value: MyObject? = delegate.read(reader)
return value?.copy(message = "custom-prefix: ${value.message}")
}
}
#Suppress("UNCHECKED_CAST") // safe due to type check at beginning
return adapter as TypeAdapter<T>
}
})
.create()
val retrofit = Retrofit.Builder()
.baseUrl("http://localhost:$port/")
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
val service = retrofit.create(DemoService::class.java)
val response = service.demo().execute()
println(response.body())
}
The following dependencies were used:
// For demo server
implementation("org.slf4j:slf4j-simple:2.0.0")
implementation("io.ktor:ktor-server-core:2.1.0")
implementation("io.ktor:ktor-server-netty:2.1.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.code.gson:gson:2.9.1")

Testing custom exceptions in Kotlin with JUnit

I have the following class
data class CarDefects(
private val _carModel: CarModel,
private val _affectedYearsOfIssue: List<Year>,
private val _defectCode: String
) {
init {
validateDefectCode(_defectCode)
}
}
Validating function
fun validateDefectCode(defectCode: String) {
val pattern = Pattern.compile("^[a-zA-Z0-9-]*\$")
val m = pattern.matcher(defectCode)
if (defectCode.length !in 4..4) {
throw InvalidDefectCodeException(defectCode, "Defect code must be 4 characters long")
}
if (!m.matches()) {
throw InvalidDefectCodeException(defectCode, "Defect code can only contain alphanumeric characters")
}
}
And the exception class:
class InvalidDefectCodeException(_defectCode:String, message:String):
IllegalArgumentException("Invalid defect code $_defectCode. $message") {
}
I'm trying to test the validating function with JUnit
import car.exceptions.InvalidDefectCodeException
import car.validators.carDefectsValidators.validateDefectCode
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import java.time.Year
import kotlin.test.assertFailsWith
internal class CarDefectsTest {
val carModel = CarModel(Brand.BMW, "X5", 199219)
val carModel2 = CarModel(Brand.AUDI, "X434", 199219)
val defect = CarDefects(carModel, listOf(Year.of(2020), Year.of(2021)), "SE2#")
val defect2 = CarDefects(carModel2, listOf(Year.of(2020), Year.of(2021)), "122F4")
#Test
fun testDefectCodeExceptions() {
val exception = Assertions.assertThrows(InvalidDefectCodeException::class.java) {
validateDefectCode(defect.getDefectCode())
}
}
#Test
fun testDefectCodeExceptions2() {
assertFailsWith<InvalidDefectCodeException> {
validateDefectCode(defect2.getDefectCode())
}
}
}
Both tests fail, however expected exceptions are still thrown, from what i understand shouldn't both tests pass?
I've already seen the following post: Test expected exceptions in Kotlin
Inside class CarDefects, you're having this init block:
init {
validateDefectCode(_defectCode)
}
Hence, the exception will be thrown during construction.
Let's test the constructor instead with a stripped down CarDefects class. The following tests are passing on my computer.
import car.exceptions.InvalidDefectCodeException
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import kotlin.test.assertFailsWith
data class CarDefects(
private val defectCode: String
) {
init {
validateDefectCode(defectCode)
}
}
internal class CarDefectsTest {
#Test
fun testDefectCodeExceptions() {
Assertions.assertThrows(InvalidDefectCodeException::class.java) {
CarDefects(defectCode = "SE2#")
}
}
#Test
fun testDefectCodeExceptions2() {
assertFailsWith<InvalidDefectCodeException> {
CarDefects(defectCode = "122F4")
}
}
}

kotlin flow is not emitting values from different function

I am trying to implement kotlin stateflow, but not able to know the reason why it is not working.
Current output:
verificatio 34567
Expected Output:
verificatio 34567
verificatio failed
package stateflowDuplicate
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val firebasePhoneVerificationListener = FirebaseOTPVerificationOperation1()
val oTPVerificationViewModal = OTPVerificationViewModal1(firebasePhoneVerificationListener)
oTPVerificationViewModal.fail()
}
class OTPVerificationViewModal1(private val firebasePhoneVerificationListener: FirebaseOTPVerificationOperation1) {
init {
startPhoneNumberVerification()
setUpListener()
}
suspend fun fail(){
firebasePhoneVerificationListener.fail()
}
private fun startPhoneNumberVerification() {
firebasePhoneVerificationListener.initiatePhoneVerification("34567")
}
private fun setUpListener() {
runBlocking {
firebasePhoneVerificationListener.phoneVerificationFailed.collect {
println("verificatio $it")
}
}
}
}
Second class
package stateflowDuplicate
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.runBlocking
class FirebaseOTPVerificationOperation1 {
private val _phoneVerificationFailed = MutableStateFlow<String?>(null)
val phoneVerificationFailed: StateFlow<String?>
get() = _phoneVerificationFailed
fun initiatePhoneVerification(phoneNumber: String) {
_phoneVerificationFailed.value = phoneNumber
}
suspend fun fail() {
delay(200L)
_phoneVerificationFailed.value = "failed"
}
}
Tried to understand the concept from these links,
Link1
Link2
You have to start a new coroutine to call collect because the coroutine will keep collecting values until its Job gets cancelled. Don't use runBlocking builder for that, use launch builder instead:
private fun setUpListener() = launch {
firebasePhoneVerificationListener.phoneVerificationFailed.collect {
println("verificatio $it")
}
}
Now to make it work you need to implement CoroutineScope interface in your class. You can do it like this:
class OTPVerificationViewModal1(
private val firebasePhoneVerificationListener: FirebaseOTPVerificationOperation1
): CoroutineScope by CoroutineScope(Dispatchers.Default) {
...
}
If you run it now you will get this output:
verificatio 34567
verificatio failed

Tests fail when run together but succeed individually even when instances are re-mocked before each test

I've looked at this SO post and made sure my tests don't share the same mocked instances, but the tests still fail when run together but succeed when run individually.
I suspect there might be something that prevents my instances from being re-mocked after every test, but I'm not experienced enough with Mockito to identify the issue
Here is my test class
package com.relic.viewmodel
import android.arch.core.executor.testing.InstantTaskExecutorRule
import android.arch.lifecycle.Observer
import com.nhaarman.mockitokotlin2.*
import com.relic.api.response.Data
import com.relic.api.response.Listing
import com.relic.data.PostRepository
import com.relic.data.UserRepository
import com.relic.data.gateway.PostGateway
import com.relic.domain.models.ListingItem
import com.relic.domain.models.UserModel
import com.relic.presentation.displayuser.DisplayUserVM
import com.relic.presentation.displayuser.ErrorData
import com.relic.presentation.displayuser.UserTab
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.setMain
import org.junit.Before
import org.junit.Rule
import org.junit.Test
#ExperimentalCoroutinesApi
class UserVMTest {
#get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var postRepo : PostRepository
private lateinit var userRepo : UserRepository
private lateinit var postGateway : PostGateway
private val username = "testUsername"
init {
val mainThreadSurrogate = newSingleThreadContext("Test thread")
Dispatchers.setMain(mainThreadSurrogate)
}
#Before
fun setup() {
postRepo = mock()
userRepo = mock()
postGateway = mock()
}
#Test
fun `user retrieved on init`() = runBlocking {
val mockUser = mock<UserModel>()
whenever(userRepo.retrieveUser(username)).doReturn(mockUser)
val vm = DisplayUserVM(postRepo, userRepo, postGateway, username)
val observer : Observer<UserModel> = mock()
vm.userLiveData.observeForever(observer)
verify(userRepo, times(1)).retrieveUser(username)
verify(observer).onChanged(mockUser)
}
#Test
fun `livedata updated when posts retrieved` () = runBlocking {
val mockListingItems = listOf<ListingItem>(mock())
val listing = mockListing(mockListingItems)
whenever(postRepo.retrieveUserListing(any(), any(), any())).doReturn(listing)
val tab = UserTab.Saved
val vm = DisplayUserVM(postRepo, userRepo, postGateway, username)
val observer : Observer<List<ListingItem>> = mock()
vm.getTabPostsLiveData(tab).observeForever(observer)
vm.requestPosts(tab, true)
verify(postRepo, times(1)).retrieveUserListing(any(), any(), any())
verify(observer, times(1)).onChanged(mockListingItems)
}
#Test
fun `error livedata updated when no posts retrieved` () = runBlocking {
val listing = mockListing()
val localPostRepo = postRepo
whenever(localPostRepo.retrieveUserListing(any(), any(), any())).doReturn(listing)
val tab = UserTab.Saved
val vm = DisplayUserVM(postRepo, userRepo, postGateway, username)
val listingObserver : Observer<List<ListingItem>> = mock()
vm.getTabPostsLiveData(tab).observeForever(listingObserver)
val errorObserver : Observer<ErrorData> = mock()
vm.errorLiveData.observeForever(errorObserver)
vm.requestPosts(tab, true)
verify(postRepo, times(1)).retrieveUserListing(any(), any(), any())
// listing livedata shouldn't be updated, but an "error" should be posted
verify(listingObserver, never()).onChanged(any())
verify(errorObserver, times(1)).onChanged(ErrorData.NoMorePosts(tab))
}
private fun mockListing(
listingItems : List<ListingItem> = emptyList()
) : Listing<ListingItem> {
val data = Data<ListingItem>().apply {
children = listingItems
}
return Listing(kind = "", data = data)
}
}
Ok so after a bit more reading, it I've updated my code so that it uses the TestCoroutineDispatcher instead of newSingleThreadContext(). I also added a teardown method per the instructions here https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/
#After
fun teardown() {
Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
mainThreadSurrogate.cleanupTestCoroutines()
}
Tests run successfully now

Mocking ViewModel in Espresso

I'm writing Espresso UI test which mocks viewModel, referring GithubBrowserSample
what is the use of "TaskExecutorWithIdlingResourceRule", declaring Junit Rule will take care of IdlingResource?
Even after referring this "TaskExecutorWithIdlingResourceRule" class in my project whenever I build, compiler doesn't throw any error but when I run the test case it shows the Unresolved Error(s)
TaskExecutorWithIdlingResourceRule.kt
import androidx.arch.core.executor.testing.CountingTaskExecutorRule
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import org.junit.runner.Description
import java.util.UUID
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
class TaskExecutorWithIdlingResourceRule : CountingTaskExecutorRule() {
// give it a unique id to workaround an espresso bug where you cannot register/unregister
// an idling resource w/ the same name.
private val id = UUID.randomUUID().toString()
private val idlingResource: IdlingResource = object : IdlingResource {
override fun getName(): String {
return "architecture components idling resource $id"
}
override fun isIdleNow(): Boolean {
return this#TaskExecutorWithIdlingResourceRule.isIdle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
callbacks.add(callback)
}
}
private val callbacks = CopyOnWriteArrayList<IdlingResource.ResourceCallback>()
override fun starting(description: Description?) {
IdlingRegistry.getInstance().register(idlingResource)
super.starting(description)
}
override fun finished(description: Description?) {
drainTasks(10, TimeUnit.SECONDS)
callbacks.clear()
IdlingRegistry.getInstance().unregister(idlingResource)
super.finished(description)
}
override fun onIdle() {
super.onIdle()
for (callback in callbacks) {
callback.onTransitionToIdle()
}
}
}
Mocktest
#RunWith(AndroidJUnit4::class)
class MockTest {
#Rule
#JvmField
var activityRule = IntentsTestRule(SingleFragmentActivity::class.java, true, true)
#Rule
#JvmField
val executorRule = TaskExecutorWithIdlingResourceRule()
private lateinit var viewModel: SeriesFragmentViewModel
private val uiModelList = mutableListOf<SeriesBaseUIModel>()
private val seriesMutableLiveData = MutableLiveData<List<SeriesBaseUIModel>>()
private val seriesFragment = SeriesFragment()
#Before
fun init(){
viewModel = mock(SeriesFragmentViewModel::class.java)
`when`(viewModel.seriesLiveData).thenReturn(seriesMutableLiveData)
ViewModelUtil.createFor(viewModel)
activityRule.activity.setFragment(seriesFragment)
EspressoTestUtil.disableProgressBarAnimations(activityRule)
}
#Test
fun testLoading()
{
//Thread.sleep(3000)
uiModelList.add(ProgressUIModel())
seriesMutableLiveData.postValue(uiModelList.toList())
onView(withId(R.id.pod_series_recycler_view))
.check(selectedDescendantsMatch(withId(R.id.pod_adapter_series_header_title), isDisplayed()))
onView(withId(R.id.pod_series_recycler_view))
.check(selectedDescendantsMatch(withId(R.id.pod_adapter_series_header_title), withText(R.string.pod_series_header_title_text)))
onView(withId(R.id.pod_series_recycler_view))
.check(selectedDescendantsMatch(withId(R.id.pod_adapter_series_header_description), isDisplayed()))
onView(withId(R.id.pod_series_recycler_view))
.check(selectedDescendantsMatch(withId(R.id.pod_adapter_series_header_title), withText("Hello")))
Thread.sleep(5000)
}
}