kotlin 使用Repository + LiveData + Coroutines进行ViewModel单元测试

elcex8rz  于 2023-11-21  发布在  Kotlin
关注(0)|答案(3)|浏览(161)

所以我对如何实现ViewModel的单元测试感到困惑,我正在使用reflect来获取和使用存储库的API。
ViewModel.kt

@HiltViewModel
class MoviesViewModel @Inject constructor(private val moviesRepository: MoviesRepository) :
ViewModel() {
private val _navigatetoDetail = MutableLiveData<Movies?>()

fun getPopularMovies() = liveData(Dispatchers.Default) {
    emit(Resource.loading(null))
    try {
        emit(Resource.success(moviesRepository.getPopularMovies()))
    } catch (e: Exception) {
        emit(
            Resource.error(
                null,
                e.message ?: "Unknown Error"
            )
        )
        Log.e("viewModel", "popularMovies error: ${e.message}")
    }
}

fun getMovieDetails(movie_id: String) = liveData(Dispatchers.Default) {
    emit(Resource.loading(null))
    try {
        emit(Resource.success(moviesRepository.getMovieDetails(movie_id)))
    } catch (e: Exception) {
        emit(
            Resource.error(
                null,
                e.message ?: "Unknown Error"
            )
        )
        Log.e("viewModel", "movieDetails error: ${e.message}")
    }
}

fun navigatetoDetail(): LiveData<Movies?> {
    return _navigatetoDetail
}

fun onMovieClicked(movies: Movies?) {
    _navigatetoDetail.value = movies
}

fun onMovieDetailNavigated() {
    _navigatetoDetail.value = null
}

字符串
Repository.kt

class MoviesRepository @Inject constructor(private var apiInterface: ApiInterface) {
init {
    apiInterface = ApiBuilder.createService()
}

suspend fun getPopularMovies() = apiInterface.getPopularMovies()

suspend fun getMovieDetails(movie_id: String) = apiInterface.getMovieDetails(movie_id)

suspend fun getPopularTvShows() = apiInterface.getPopularTvShows()

suspend fun getTvShowDetails(tvshow_id: String) = apiInterface.getTvShowDetails(tvshow_id)


apiInterface.kt

interface ApiInterface {
@GET("/3/movie/popular?api_key=$API_KEY&language=en-US")
suspend fun getPopularMovies(): Envelope<List<Movies>>

@GET("/3/movie/{movie_id}?api_key=$API_KEY&language=en-US")
suspend fun getMovieDetails(@Path("movie_id") movie_id: String?): Movies

@GET("3/tv/popular?api_key=$API_KEY&language=en-US&page=1")
suspend fun getPopularTvShows(): Envelope<List<TvShows>>

@GET("/3/tv/{tvshow_id}?api_key=$API_KEY&language=en-US")
suspend fun getTvShowDetails(@Path("tvshow_id") tvshow_id: String?): TvShows


我已经试着通过这样做来测试我的ViewModel:

@Test
fun testGetPopularMovies() = coroutinesTestRule.testDispatcher.runBlockingTest {
    val moviesList = viewModel.getPopularMovies().value
    viewModel.getPopularMovies().observeForever(observer)
    verify(observer).onChanged(argumentCaptor.capture())
    assertEquals(20, moviesList?.data?.results?.size)
}


但它在 viewModel.getPopularMovies().ObserveForever(observer) 上返回NullPointerException

rryofs0p

rryofs0p1#

这里是一个用于等待实时数据结果的扩展函数。使用下面的代码在测试包下创建koltin扩展函数。

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException

@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()
        [email protected](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
}

字符串
这里是视图模型类测试示例。LoginViewmodel类扩展AndroidViewModel而不是ViewModel。

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.mymaskreminder.ui.getOrAwaitValue
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class LoginViewModelTest {

@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()

@Test
fun sendLoginRequest() {

    val loginViewModel = LoginViewModel(ApplicationProvider.getApplicationContext())

    loginViewModel.sendLoginRequest("[email protected]", "12345")

    //shadowOf(Looper.getMainLooper()).idle()

    val value_error = loginViewModel.loginErrorResponse.getOrAwaitValue()

    Assert.assertEquals(value_error.toString(),
        "Unauthorized !"
    )


}
}

nkoocmlb

nkoocmlb2#

你需要有一个testCoroutineDispatcher或testCoroutineScope来设置你的viewModel的范围到测试的范围。添加这个类:

@ExperimentalCoroutinesApi
class CoroutineTestRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {

override fun starting(description: Description) {
    super.starting(description)
    Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher)
}

override fun finished(description: Description) {
    super.finished(description)
    Dispatchers.resetMain()
}

}

字符串
并在测试类中使用:

@SmallTest
@RunWith(PowerMockRunner::class)
class BaseRepositoryTest {

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()

    @get:Rule
    var coroutineTestRule = CoroutineTestRule()

}


不要忘记使用runBlockingTest来测试函数。runBlockingTest在coroutinesTest库中可用:
testImplementation(“org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3”)

@Test
    fun `success user login`() {
        runBlockingTest {
            viewModel.sendLoginRequest(PHONE_VALID_NUMBER1)
            val result = viewModel.liveData.getOrAwaitValueTest()
            assert(result == LoginViewModel.Views(PHONE_VALID_NUMBER1))

        }
    }


还使用getOrWaitValue观察API结果

wf82jlnq

wf82jlnq3#

确保你在单元测试/模拟中是来自流的emitting值。如果你不发出值,那么你在ViewModel中的LiveData将永远不会被设置任何值,因为没有什么可收集的。你在LiveData上设置值的视图模型中的collect内的代码将不会被执行。
另一种选择是使用TestDispatcher如果你使用的是StandardTestDispatcher,那么你需要从调用advanceUntilIdle()函数来执行在测试中调度的协程TestCoroutine。
如果你使用的是UnconfinedTestDispatcher,那么你可以删除advanceUntilIdle(),因为它开始急切地执行协程。
协同程序测试库

testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")

字符串
方法1

@Test
fun testMethod1() = runTest {
   val observer = Observer<Result> { result ->
    // put your assert statements here
    }
   viewModel.liveData.observeForever(observer)
   viewModel.fetchSomeData()
   advanceUntilIdle()
   viewModel.liveData.removeObserver(observer)
}


方法2

@Test
    fun testMethod2() = runTest {
        viewModel.fetchSomeData()
        advanceUntilIdle()

        val result = viewModel.liveData.value
      
        assertNotNull(result)
        // Write your assert statements here
    }


方法3
通过使用android架构组件中提供的getOrAwaitValue()示例
https://github.com/android/architecture-components-samples/blob/master/LiveDataSample/app/src/test/java/com/android/example/livedatabuilder/util/LiveDataTestUtil.kt

@Test
fun testMethod3() = runTest {
    val result = viewModel.liveData.getOrAwaitValue {
       viewModel.fetchSomeData()
       advanceUntilIdle()
    }
     
    assertNotNull(result)
    // put your assert statements here
}

相关问题