英文:
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<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)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论