有人可以向我展示如何使此viewModel中的getMovies函数可测试吗?我无法获得单元测试以正确等待协程。
(1)我很确定我必须创建一个test-CoroutineScope和一个正常的lifeCycle-CoroutineScope,如this Medium Article所示。
(2)一旦定义了范围,我也不确定如何在给定正常的应用程序上下文或测试上下文的情况下,告诉getMovies()应该使用哪个范围。
enum class MovieApiStatus { LOADING, ERROR, DONE }
class MovieListViewModel : ViewModel() {
var pageCount = 1
private val _status = MutableLiveData<MovieApiStatus>()
val status: LiveData<MovieApiStatus>
get() = _status
private val _movieList = MutableLiveData<List<Movie>>()
val movieList: LiveData<List<Movie>>
get() = _movieList
// allows easy update of the value of the MutableLiveData
private var viewModelJob = Job()
// the Coroutine runs using the Main (UI) dispatcher
private val coroutineScope = CoroutineScope(
viewModelJob + Dispatchers.Main
)
init {
Log.d("list", "in init")
getMovies(pageCount)
}
fun getMovies(pageNumber: Int) {
coroutineScope.launch {
val getMoviesDeferred =
MovieApi.retrofitService.getMoviesAsync(page = pageNumber)
try {
_status.value = MovieApiStatus.LOADING
val responseObject = getMoviesDeferred.await()
_status.value = MovieApiStatus.DONE
............
} catch (e: Exception) {
_status.value = MovieApiStatus.ERROR
................
}
}
pageCount = pageNumber.inc()
}
...
}
它使用此API服务...
package com.example.themovieapp.network
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.Deferred
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
private const val BASE_URL = "https://api.themoviedb.org/3/"
private const val API_key = ""
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(BASE_URL)
.build()
interface MovieApiService{
//https://developers.themoviedb.org/3/movies/get-top-rated-movies
//https://square.github.io/retrofit/2.x/retrofit/index.html?retrofit2/http/Query.html
@GET("movie/top_rated")
fun getMoviesAsync(
@Query("api_key") apiKey: String = API_key,
@Query("language") language: String = "en-US",
@Query("page") page: Int
): Deferred<ResponseObject>
}
/*
Because this call is expensive, and the app only needs
one Retrofit service instance, you expose the service to the rest of the app using
a public object called MovieApi, and lazily initialize the Retrofit service there
*/
object MovieApi {
val retrofitService: MovieApiService by lazy {
retrofit.create(MovieApiService::class.java)
}
}
我只是试图创建一个测试,该测试在功能之后断言liveData'status'已完成。
这是Project Repository
最佳答案
首先,您需要以某种方式使协程作用域可注入(inject),方法是手动为其创建提供程序,或者使用诸如dagger之类的注入(inject)框架。这样,在测试ViewModel时,可以使用测试版本覆盖协程范围。
有几种选择可以执行此操作,您可以简单地使ViewModel本身可注入(inject)(此处的文章:https://medium.com/chili-labs/android-viewmodel-injection-with-dagger-f0061d3402ff)
或者,您可以手动创建ViewModel提供程序,并在创建它的任何地方使用它。无论如何,我强烈建议采用某种形式的依赖项注入(inject),以实现真正的可测试性。
无论如何,您的ViewModel需要具有提供的的CoroutineScope ,而不是实例化协程范围本身。
换句话说,您可能想要
class MovieListViewModel(val couroutineScope: YourCoroutineScope) : ViewModel() {}
或许
class MovieListViewModel @Inject constructor(val coroutineScope: YourCoroutineScope) : ViewModel() {}
无论您做什么注入(inject),下一步都是创建自己的CoroutineScope接口(interface),您可以在测试上下文中覆盖该接口(interface)。例如:
interface YourCoroutineScope : CoroutineScope {
fun launch(block: suspend CoroutineScope.() -> Unit): Job
}
这样,当您为应用程序使用范围时,可以使用一个范围,例如生命周期协程范围:
class LifecycleManagedCoroutineScope(
private val lifecycleCoroutineScope: LifecycleCoroutineScope,
override val coroutineContext: CoroutineContext = lifecycleCoroutineScope.coroutineContext) : YourCoroutineScope {
override fun launch(block: suspend CoroutineScope.() -> Unit): Job = lifecycleCoroutineScope.launchWhenStarted(block)
}
在测试中,您可以使用测试范围:
class TestScope(override val coroutineContext: CoroutineContext) : YourCoroutineScope {
val scope = TestCoroutineScope(coroutineContext)
override fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return scope.launch {
block.invoke(this)
}
}
}
现在,由于ViewModel使用的是类型YourCoroutineScope的范围,并且由于在以上示例中,生命周期和测试版本都实现了YourCoroutineScope接口(interface),因此您可以在不同情况下使用范围的不同版本,即app与test。