[英]ViewModel Unit testing multiple view states with LiveData, Coroutines and MockK
[英]ViewModel unit testing with Repository + LiveData + Coroutines
所以我对如何对我的 ViewModel 实施单元测试感到困惑。 我正在使用 retrofit 来获取和使用存储库的 API。
视图模型.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
}
存储库.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)
api接口.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
这是一个扩展 function 用于等待实时数据结果。 在您的测试 package 下使用以下代码创建 koltin 扩展 function。
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()
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
}
这是示例视图 model class 测试。LoginViewmodel class 扩展了 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("test@gmail.com", "12345")
//shadowOf(Looper.getMainLooper()).idle()
val value_error = loginViewModel.loginErrorResponse.getOrAwaitValue()
Assert.assertEquals(value_error.toString(),
"Unauthorized !"
)
}
}
您需要有一个 testCoroutineDispatcher 或 testCoroutineScope 才能将您的 viewModel 的 scope 设置为测试的 scope。 添加此 class:
@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()
}
}
并用于测试 class:
@SmallTest
@RunWith(PowerMockRunner::class)
class BaseRepositoryTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
var coroutineTestRule = CoroutineTestRule()
}
不要忘记将 runBlockingTest 用于测试功能。 coroutinesTest 库上可用的 runBlockingTest:
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))
}
}
也使用 getOrAwaitValue 来观察 api 结果
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.