在Kotlin中测试CoroutineScope基础架构 [英] Test CoroutineScope infrastructure in Kotlin

查看:141
本文介绍了在Kotlin中测试CoroutineScope基础架构的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有人可以向我展示如何使此viewModel中的getMovies函数可测试吗?我无法进行单元测试来正确等待协程..

would someone be able to show me how to make the getMovies function in this viewModel testable? I can't get the unit tests to await the coroutines properly..

(1)我很确定我必须创建一个test-CoroutineScope和一个正常的lifeCycle-CoroutineScope,如

(1) I'm pretty sure I have to create a test-CoroutineScope and a normal lifeCycle-CoroutineScope, as seen in this Medium Article.

(2)一旦定义了范围,我也不确定如何在给定正常的应用程序上下文或测试上下文的情况下,告诉getMovies()应该使用哪个范围.

(2) Once the scope definitions are made, I'm also unsure how to tell getMovies() which scope it should be using given a normal app context or a test context.

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服务...

it uses this API service...

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的状态"在该函数之后已完成.

I'm simply trying to create a test which asserts the liveData 'status' is DONE after the function.

这是项目存储库

推荐答案

首先,您需要通过手动为其创建提供程序或使用诸如dagger之类的注入框架,使协程范围可注入.这样,当您测试ViewModel时,可以使用测试版本覆盖协程范围.

First you need to make your coroutine scope injectable somehow, either by creating a provider for it manually, or using an injection framework like dagger. That way, when you test your ViewModel, you can override the coroutine scope with a test version.

执行此操作有几种选择,您可以简单地使ViewModel本身可注入(此处的文章:

There are a few choices to do this, you can simply make the ViewModel itself injectable (article on that here: https://medium.com/chili-labs/android-viewmodel-injection-with-dagger-f0061d3402ff)

或者您可以手动创建ViewModel提供程序,并在创建它的任何地方使用它.无论如何,我强烈建议采用某种形式的依赖注入,以实现真正的可测试性.

Or you can manually create a ViewModel provider and use that where ever it's created. No matter what, I would strongly advise some form of dependency injection in order to achieve real testability.

无论如何,您的ViewModel需要提供其CoroutineScope,而不是实例化coroutine范围本身.

Regardless, your ViewModel needs to have its CoroutineScope provided, not instantiate the coroutine scope itself.

换句话说,您可能想要

class MovieListViewModel(val couroutineScope: YourCoroutineScope) : ViewModel() {}

或者也许

class MovieListViewModel @Inject constructor(val coroutineScope: YourCoroutineScope) : ViewModel() {}

无论您做什么注入,下一步都是创建自己的CoroutineScope接口,您可以在测试上下文中覆盖该接口.例如:

No matter what you do for injection, the next step is to create your own CoroutineScope interface that you can override in the test context. For example:

interface YourCoroutineScope : CoroutineScope {
    fun launch(block: suspend CoroutineScope.() -> Unit): Job
}

这样,当您为应用程序使用范围时,可以使用一个范围,例如生命周期协程范围:

That way when you use the scope for your app, you can use one scope, say, lifecycle coroutine scope:

class LifecycleManagedCoroutineScope(
        private val lifecycleCoroutineScope: LifecycleCoroutineScope,
        override val coroutineContext: CoroutineContext = lifecycleCoroutineScope.coroutineContext) : YourCoroutineScope {
    override fun launch(block: suspend CoroutineScope.() -> Unit): Job = lifecycleCoroutineScope.launchWhenStarted(block)
}

对于您的测试,您可以使用测试范围:

And for your test, you can use a test scope:

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接口,因此您可以在不同情况下使用范围的不同版本,例如app vs测试.

Now, since your ViewModel is using a scope of type YourCoroutineScope, and since, in the examples above, both the lifecycle and test version implement the YourCoroutineScope interface, you can use different versions of the scope in different situations, i.e. app vs test.

这篇关于在Kotlin中测试CoroutineScope基础架构的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆