Unit testing 在Kotlin测试CoroutineScope基础设施
有人能告诉我如何使这个viewModel中的getMovies功能可测试吗?我无法让单元测试正确地等待协同程序 1我非常确定我必须创建一个测试协同作用域和一个正常的生命周期协同作用域,如中所示 2一旦定义了范围,我也不确定如何告诉getMovies在给定正常应用程序上下文或测试上下文的情况下应该使用哪个范围Unit testing 在Kotlin测试CoroutineScope基础设施,unit-testing,android-studio,kotlin,testing,kotlin-coroutines,Unit Testing,Android Studio,Kotlin,Testing,Kotlin Coroutines,有人能告诉我如何使这个viewModel中的getMovies功能可测试吗?我无法让单元测试正确地等待协同程序 1我非常确定我必须创建一个测试协同作用域和一个正常的生命周期协同作用域,如中所示 2一旦定义了范围,我也不确定如何告诉getMovies在给定正常应用程序上下文或测试上下文的情况下应该使用哪个范围 enum class MovieApiStatus { LOADING, ERROR, DONE } class MovieListViewModel : ViewModel() {
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的“状态”是在函数之后完成的
首先,您需要以某种方式使您的协同程序范围可注入,可以手动为其创建提供者,也可以使用类似dagger的注入框架。这样,当您测试ViewModel时,可以使用测试版本覆盖协同程序范围 有几种选择可以实现这一点,您可以在这里简单地将ViewModel本身作为可注入文章: 或者,您可以手动创建ViewModel提供程序,并在创建时使用该提供程序。无论如何,我强烈建议采用某种形式的依赖注入,以实现真正的可测试性 无论如何,您的ViewModel需要提供它的协程作用域,而不是实例化协程作用域本身 换句话说,你可能想要
class MovieListViewModel(val couroutineScope: YourCoroutineScope) : ViewModel() {}
或许
class MovieListViewModel @Inject constructor(val coroutineScope: YourCoroutineScope) : ViewModel() {}
无论您对注入做什么,下一步都是创建您自己的CoroutineScope接口,您可以在测试上下文中重写该接口。例如:
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接口,因此您可以在不同的情况下使用不同版本的作用域,即应用与测试。首先,您需要以某种方式使您的coroutine作用域可注入,通过手动为其创建提供者,或者使用dagger之类的注入框架。这样,当您测试ViewModel时,可以使用测试版本覆盖协同程序范围 有几种选择可以实现这一点,您可以在这里简单地将ViewModel本身作为可注入文章: 或者,您可以手动创建ViewModel提供程序,并在创建时使用该提供程序。无论如何,我强烈建议采用某种形式的依赖注入,以实现真正的可测试性 无论如何,您的ViewModel需要提供它的协程作用域,而不是实例化协程作用域本身 换句话说,你可能想要
class MovieListViewModel(val couroutineScope: YourCoroutineScope) : ViewModel() {}
或许
class MovieListViewModel @Inject constructor(val coroutineScope: YourCoroutineScope) : ViewModel() {}
无论您对注入做什么,下一步都是创建您自己的CoroutineScope接口,您可以在测试上下文中重写该接口。例如:
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接口,因此您可以在不同的情况下使用不同版本的作用域,即应用程序与测试。好的,感谢您的回答,我能够编写一些测试,这些测试似乎正在正确地等待函数
这是我所做的事情的副本:
enum class MovieApiStatus { LOADING, ERROR, DONE }
class MovieListViewModel(val coroutineScope: ManagedCoroutineScope) : ViewModel() {
//....creating vars, livedata etc.
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
if (_movieList.value == null) {
_movieList.value = ArrayList()
}
pageCount = pageNumber.inc()
_movieList.value = movieList.value!!.toList().plus(responseObject.results)
.sortedByDescending { it.vote_average }
} catch (e: Exception) {
_status.value = MovieApiStatus.ERROR
_movieList.value = ArrayList()
}
}
fun onLoadMoreMoviesClicked() =
getMovies(pageCount)
//...nav functions, clearing functions etc.
}
下面是测试用例
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class MovieListViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = TestCoroutineDispatcher()
private val managedCoroutineScope: ManagedCoroutineScope = TestScope(testDispatcher)
lateinit var viewModel: MovieListViewModel
@Before
fun setup() {
//resProvider.mockColors()
Dispatchers.setMain(testDispatcher)
viewModel = MovieListViewModel(managedCoroutineScope)
}
@After
fun tearDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
@ExperimentalCoroutinesApi
@Test
fun getMoviesTest() {
managedCoroutineScope.launch {
assertTrue(
"initial List, API status: ${viewModel.status.getOrAwaitValue()}",
viewModel.status.getOrAwaitValue() == MovieApiStatus.DONE
)
assertTrue(
"movieList has ${viewModel.movieList.value?.size}, != 20",
viewModel.movieList.value?.size == 20
)
assertTrue(
"pageCount = ${viewModel.pageCount}, != 2",
viewModel.pageCount == 2
)
viewModel.onLoadMoreMoviesClicked()
assertTrue(
"added to list, API status: ${viewModel.status.getOrAwaitValue()}",
viewModel.status.getOrAwaitValue() == MovieApiStatus.DONE
)
assertTrue(
"movieList has ${viewModel.movieList.value?.size}, != 40",
viewModel.movieList.value?.size == 40
)
}
}
}
使用示波器需要反复试验。。runBlockingTest{}导致了“异常:作业未完成”问题
我还必须创建一个viewModel工厂,以便片段在应用程序正常运行时创建viewModel
好的,感谢您的回答,我能够编写一些测试,这些测试似乎正在正确地等待函数
这是我所做的事情的副本:
enum class MovieApiStatus { LOADING, ERROR, DONE }
class MovieListViewModel(val coroutineScope: ManagedCoroutineScope) : ViewModel() {
//....creating vars, livedata etc.
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
if (_movieList.value == null) {
_movieList.value = ArrayList()
}
pageCount = pageNumber.inc()
_movieList.value = movieList.value!!.toList().plus(responseObject.results)
.sortedByDescending { it.vote_average }
} catch (e: Exception) {
_status.value = MovieApiStatus.ERROR
_movieList.value = ArrayList()
}
}
fun onLoadMoreMoviesClicked() =
getMovies(pageCount)
//...nav functions, clearing functions etc.
}
下面是测试用例
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class MovieListViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = TestCoroutineDispatcher()
private val managedCoroutineScope: ManagedCoroutineScope = TestScope(testDispatcher)
lateinit var viewModel: MovieListViewModel
@Before
fun setup() {
//resProvider.mockColors()
Dispatchers.setMain(testDispatcher)
viewModel = MovieListViewModel(managedCoroutineScope)
}
@After
fun tearDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
@ExperimentalCoroutinesApi
@Test
fun getMoviesTest() {
managedCoroutineScope.launch {
assertTrue(
"initial List, API status: ${viewModel.status.getOrAwaitValue()}",
viewModel.status.getOrAwaitValue() == MovieApiStatus.DONE
)
assertTrue(
"movieList has ${viewModel.movieList.value?.size}, != 20",
viewModel.movieList.value?.size == 20
)
assertTrue(
"pageCount = ${viewModel.pageCount}, != 2",
viewModel.pageCount == 2
)
viewModel.onLoadMoreMoviesClicked()
assertTrue(
"added to list, API status: ${viewModel.status.getOrAwaitValue()}",
viewModel.status.getOrAwaitValue() == MovieApiStatus.DONE
)
assertTrue(
"movieList has ${viewModel.movieList.value?.size}, != 40",
viewModel.movieList.value?.size == 40
)
}
}
}
使用示波器需要反复试验。。runBlockingTest{}导致了“异常:作业未完成”问题
我还必须创建一个viewModel工厂,以便片段在应用程序正常运行时创建viewModel