如何使用Junit、Mockito和Kotlin flows在Android中测试ViewModel

huangapple go评论81阅读模式
英文:

How to test the ViewModel using Junit, Mockito and Kotlin flows in Android

问题

// 在使用 Junit 和 Mockito 进行 ViewModel 测试时,我尝试过,但遇到了空指针异常。它返回无法调用 flow 收集器。我想测试 MpinDataKey 中返回的响应,检查所有参数是否在 MpinDataKey 中,以及 API 调用等。

// 收到以下异常:

// 在线程中的异常“Test worker @coroutine#2” java.lang.NullPointerException: 无法调用“kotlinx.coroutines.flow.Flow.collect(kotlinx.coroutines.flow.FlowCollector, kotlin.coroutines.Continuation)”
LiveData 值从未设置
java.util.concurrent.TimeoutException: LiveData 值从未设置在 LiveDataUtilTest.kt 的第 38  (LiveDataUtilTest.kt:38)

data class MpinKeyData( val token: String, val publicKey: String)

sealed class Result<T> { 
    class Loading<T>: Result<T>()
    data class Success<T>(val data: T): Result<T>()
    data class Error<T>(val errorMessage: String): Result<T>() 
} 

class LoginViewModel @Inject constructor(private val loginRepository: LoginRepository): ViewModel() {

    private var _mpinKeyResponse = MutableLiveData<Result<MpinKeyData>>()
    val mpinKeyResponse: LiveData<Result<MpinKeyData>>
        get() = _mpinKeyResponse

    
    fun getMpinkey(context: Context) {
        viewModelScope.launch {
            loginRepository.getMpinKey(context).collect() {
                _mpinKeyResponse.postValue(it)
            }
        }
    }
}

class LoginDataSource @Inject constructor() {

    suspend fun getMpinKey(context: Context): Result<MpinKeyData> { 
        return try { 
            val retrofit = RetrofitBuilder.getUnAuthRetrofit(context)
            val loginService: LoginService by lazy { retrofit.create(LoginService::class.java) } 
            val response = loginService.getMpinKey() 
            ResponseHelper.processResponse(response)
        } catch (e: Exception) {
            Result.Error(e.message ?: "Exception Found") 
        } 
    } 
}

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValueTest(
    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(value: T) {
            data = value
            latch.countDown()
            this@getOrAwaitValueTest.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // 如果 LiveData 未设置,请不要无限等待。
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

class LoginViewModelTest {

    @Mock
    private lateinit var loginRepository: LoginRepository

    @Mock
    private lateinit var loginDataSource: LoginDataSource

    private val testDispatcher = StandardTestDispatcher()

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @ExperimentalCoroutinesApi
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private val context: Context = mock(Context::class.java)

    @Before
    fun initSetUp(){
        MockitoAnnotations.openMocks(this)
        Dispatchers.setMain(testDispatcher)
    }

    @ExperimentalCoroutinesApi
    @Test
    fun test_beginEnroll_Success() = runTest{
//        Mockito.`when`(loginDataSource.getMpinKey(context)).thenReturn(Result.Success<MpinKeyData>)
        val vmodel = LoginViewModel(loginRepository)
        vmodel.getMpinkey(context)
        testDispatcher.scheduler.advanceUntilIdle()
        val  result = vmodel.mpinKeyResponse.getOrAwaitValueTest()
        Assert.assertEquals(0,result)
    }

    @ExperimentalCoroutinesApi
    @Test
    fun test_beginEnroll_Failure() = runTest{
        Mockito.`when`(loginDataSource.getMpinKey(context)).thenReturn(Result.Error("Something went wrong"))
        val vmodel = LoginViewModel(loginRepository)
        vmodel.getMpinkey(context)
        testDispatcher.scheduler.advanceUntilIdle()
        val  result = vmodel.mpinKeyResponse.getOrAwaitValueTest()
        Assert.assertEquals(1,result is Result.Error)
    }


    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }

}
英文:

I tried to test the ViewModel using Junit and Mockito, getting null pointer exception. It returns cannot invoke flow collector. I want to test the responses coming in MpinDataKey, check all parameters are there in MpinDataKey, the api call etc.

Getting following exception :

Exception in thread "Test worker @coroutine#2" java.lang.NullPointerException: Cannot invoke "kotlinx.coroutines.flow.Flow.collect(kotlinx.coroutines.flow.FlowCollector, kotlin.coroutines.Continuation)"
LiveData value was never set.
java.util.concurrent.TimeoutException: LiveData value was never set.atLiveDataUtilTestKt.getOrAwaitValueTest(LiveDataUtilTest.kt:38)

data class MpinKeyData( val token: String, val publicKey: String)
sealed class Result<T> { 
class Loading<T>: Result<T>()
data class Success<T>(val data: T): Result<T>()
data class Error<T>(val errorMessage: String): Result<T>() 
} 
class LoginViewModel @Inject constructor(private val loginRepository: LoginRepository): ViewModel() {
private var _mpinKeyResponse = MutableLiveData<Result<MpinKeyData>>()
val mpinKeyResponse: LiveData<Result<MpinKeyData>>
get() = _mpinKeyResponse
fun getMpinkey(context: Context) {
viewModelScope.launch {
loginRepository.getMpinKey(context).collect() {
_mpinKeyResponse.postValue(it)
}
}
}
}
class LoginDataSource @Inject constructor() {
suspend fun getMpinKey(context: Context): Result<MpinKeyData> { 
return try { 
val retrofit = RetrofitBuilder.getUnAuthRetrofit(context)
val loginService: LoginService by lazy { retrofit.create(LoginService::class.java) 
} 
val response = loginService.getMpinKey() ResponseHelper.processResponse(response)
}catch (e: Exception) {
Result.Error(e.message ?: "Exception Found") 
} 
} 
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValueTest(
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(value: T) {
data = value
latch.countDown()
this@getOrAwaitValueTest.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
}
class LoginViewModelTest {
@Mock
private lateinit var loginRepository: LoginRepository
@Mock
private lateinit var loginDataSource: LoginDataSource
private val testDispatcher = StandardTestDispatcher()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@ExperimentalCoroutinesApi
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val context: Context = mock(Context::class.java)
@Before
fun initSetUp(){
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(testDispatcher)
}
@ExperimentalCoroutinesApi
@Test
fun test_beginEnroll_Success() = runTest{
//        Mockito.`when`(loginDataSource.getMpinKey(context)).thenReturn(Result.Success<MpinKeyData>)
val vmodel = LoginViewModel(loginRepository)
vmodel.getMpinkey(context)
testDispatcher.scheduler.advanceUntilIdle()
val  result = vmodel.mpinKeyResponse.getOrAwaitValueTest()
Assert.assertEquals(0,result)
}
@ExperimentalCoroutinesApi
@Test
fun test_beginEnroll_Failure() = runTest{
Mockito.`when`(loginDataSource.getMpinKey(context)).thenReturn(Result.Error("Something went wrong"))
val vmodel = LoginViewModel(loginRepository)
vmodel.getMpinkey(context)
testDispatcher.scheduler.advanceUntilIdle()
val  result = vmodel.mpinKeyResponse.getOrAwaitValueTest()
Assert.assertEquals(1,result is Result.Error)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
}

答案1

得分: 0

@ExperimentalCoroutinesApi
@Test
fun test_beginEnroll_Success() = runTest{
    doReturn(flowOf(Result.Success(data = emptyList<MpinKeyData>()))).`when`(loginRepository).getMpinKey(context)
    loginRepository.getMpinKey(context).test{
        assertEquals(Result.Success(emptyList<List<MpinKeyData>>()), awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
    verify(loginRepository).getMpinKey(context)
}

@ExperimentalCoroutinesApi
@Test
fun test_beginEnroll_Failure() = runTest{

    val errorMessage = "Something went wrong"

    doReturn(flowOf(Result.Error<MpinKeyData>(errorMessage))).`when`(loginRepository).getMpinKey(context)

    loginRepository.getMpinKey(context).test {
        assertEquals(
            Result.Error<MpinKeyData>(errorMessage),
            awaitItem()
        )
        cancelAndIgnoreRemainingEvents()
    }
    verify(loginRepository).getMpinKey(context)
}
英文:
@ExperimentalCoroutinesApi
@Test
fun test_beginEnroll_Success() = runTest{
doReturn(flowOf(Result.Success(data = emptyList&lt;MpinKeyData&gt;()))).`when`(loginRepository).getMpinKey(context)
loginRepository.getMpinKey(context).test{
assertEquals(Result.Success(emptyList&lt;List&lt;MpinKeyData&gt;&gt;()), awaitItem())
cancelAndIgnoreRemainingEvents()
}
verify(loginRepository).getMpinKey(context)
}
@ExperimentalCoroutinesApi
@Test
fun test_beginEnroll_Failure() = runTest{
val errorMessage = &quot;Something went wrong&quot;
doReturn(flowOf(Result.Error&lt;MpinKeyData&gt;(errorMessage))).`when`(loginRepository).getMpinKey(context)
loginRepository.getMpinKey(context).test {
assertEquals(
Result.Error&lt;MpinKeyData&gt;(errorMessage),
awaitItem()
)
cancelAndIgnoreRemainingEvents()
}
verify(loginRepository).getMpinKey(context)
}

huangapple
  • 本文由 发表于 2023年7月17日 13:52:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/76701794.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定