Android 使用LiveData的JUnit5测试不';t执行订户';s回调

Android 使用LiveData的JUnit5测试不';t执行订户';s回调,android,junit,mockito,android-livedata,android-viewmodel,Android,Junit,Mockito,Android Livedata,Android Viewmodel,背景: 我有一个简单的应用程序,它使用RESTAPI调用获取电影列表。项目结构如下所示: Activity -> ViewModel -> Repository -> ApiService (Retrofit Interface) 活动订阅LiveData并侦听事件更改 ViewModel承载活动观察到的MediatorLiveData。最初,ViewModel在MediatorLiveData中设置一个资源。加载(..)值 ViewModel然后调用存储库从ApiSer

背景:

我有一个简单的应用程序,它使用RESTAPI调用获取电影列表。项目结构如下所示:

Activity -> ViewModel -> Repository -> ApiService (Retrofit Interface)
  • 活动订阅LiveData并侦听事件更改

  • ViewModel承载活动观察到的MediatorLiveData。最初,ViewModelMediatorLiveData中设置一个
    资源。加载(..)

  • ViewModel然后调用存储库从ApiService

  • ApiService返回
    Resource.success(…)
    Resource.error(…)

  • ViewModel然后将LiveData中的ApiService结果合并到MediatorLiveData

  • 我的查询:

    在单元测试中,只有第一个发出
    资源。加载(..)
    是由MediatorLiveDataViewModel进行的。MediatorLiveData从不从存储库发出任何数据

    ViewModel.class

    private var discoverMovieLiveData: MediatorLiveData<Resource<DiscoverMovieResponse>> = MediatorLiveData()
    
    fun observeDiscoverMovie(): LiveData<Resource<DiscoverMovieResponse>> {
            return discoverMovieLiveData
        }
    
    fun fetchDiscoverMovies(page: Int) {
    
            discoverMovieLiveData.value = Resource.loading(null) // this emit get observed immediately 
    
            val source = movieRepository.fetchDiscoverMovies(page)
            discoverMovieLiveData.addSource(source) {
                discoverMovieLiveData.value = it // never gets called
                discoverMovieLiveData.removeSource(source)
            }
        } 
    
    fun fetchDiscoverMovies(page: Int): LiveData<Resource<DiscoverMovieResponse>> {
            return LiveDataReactiveStreams.fromPublisher(
                apiService.fetchDiscoverMovies(page)
                    .subscribeOn(Schedulers.io())
                    .map { d ->
                        Resource.success(d) // never gets called in unit test
                    }
                    .onErrorReturn { e ->
                        Resource.error(ApiErrorHandler.getErrorByThrowable(e), null) // // never gets called in unit test
                    }
            )
        }
    
    class RxImmediateSchedulerRule : TestRule {
    
        private val immediate = object : Scheduler() {
            override fun createWorker(): Worker {
                return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
            }
        }
    
        override fun apply(base: Statement, description: Description): Statement {
            return object : Statement() {
                @Throws(Throwable::class)
                override fun evaluate() {
                    RxJavaPlugins.setInitIoSchedulerHandler { immediate }
                    RxJavaPlugins.setInitComputationSchedulerHandler { immediate }
                    RxJavaPlugins.setInitNewThreadSchedulerHandler { immediate }
                    RxJavaPlugins.setInitSingleSchedulerHandler { immediate }
                    RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate }
    
                    try {
                        base.evaluate()
                    } finally {
                        RxJavaPlugins.reset()
                        RxAndroidPlugins.reset()
                    }
                }
            }
        }
    
    }
    
    class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
    
        override fun beforeEach(context: ExtensionContext?) {
            ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
                override fun executeOnDiskIO(runnable: Runnable) {
                    runnable.run()
                }
    
                override fun postToMainThread(runnable: Runnable) {
                    runnable.run()
                }
    
                override fun isMainThread(): Boolean {
                    return true
                }
            })
        }
    
        override fun afterEach(context: ExtensionContext?) {
            ArchTaskExecutor.getInstance().setDelegate(null)
        }
    
    }
    
    }

    RxImmediateSchedulerRule.class

    private var discoverMovieLiveData: MediatorLiveData<Resource<DiscoverMovieResponse>> = MediatorLiveData()
    
    fun observeDiscoverMovie(): LiveData<Resource<DiscoverMovieResponse>> {
            return discoverMovieLiveData
        }
    
    fun fetchDiscoverMovies(page: Int) {
    
            discoverMovieLiveData.value = Resource.loading(null) // this emit get observed immediately 
    
            val source = movieRepository.fetchDiscoverMovies(page)
            discoverMovieLiveData.addSource(source) {
                discoverMovieLiveData.value = it // never gets called
                discoverMovieLiveData.removeSource(source)
            }
        } 
    
    fun fetchDiscoverMovies(page: Int): LiveData<Resource<DiscoverMovieResponse>> {
            return LiveDataReactiveStreams.fromPublisher(
                apiService.fetchDiscoverMovies(page)
                    .subscribeOn(Schedulers.io())
                    .map { d ->
                        Resource.success(d) // never gets called in unit test
                    }
                    .onErrorReturn { e ->
                        Resource.error(ApiErrorHandler.getErrorByThrowable(e), null) // // never gets called in unit test
                    }
            )
        }
    
    class RxImmediateSchedulerRule : TestRule {
    
        private val immediate = object : Scheduler() {
            override fun createWorker(): Worker {
                return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
            }
        }
    
        override fun apply(base: Statement, description: Description): Statement {
            return object : Statement() {
                @Throws(Throwable::class)
                override fun evaluate() {
                    RxJavaPlugins.setInitIoSchedulerHandler { immediate }
                    RxJavaPlugins.setInitComputationSchedulerHandler { immediate }
                    RxJavaPlugins.setInitNewThreadSchedulerHandler { immediate }
                    RxJavaPlugins.setInitSingleSchedulerHandler { immediate }
                    RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate }
    
                    try {
                        base.evaluate()
                    } finally {
                        RxJavaPlugins.reset()
                        RxAndroidPlugins.reset()
                    }
                }
            }
        }
    
    }
    
    class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
    
        override fun beforeEach(context: ExtensionContext?) {
            ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
                override fun executeOnDiskIO(runnable: Runnable) {
                    runnable.run()
                }
    
                override fun postToMainThread(runnable: Runnable) {
                    runnable.run()
                }
    
                override fun isMainThread(): Boolean {
                    return true
                }
            })
        }
    
        override fun afterEach(context: ExtensionContext?) {
            ArchTaskExecutor.getInstance().setDelegate(null)
        }
    
    }
    
    InstantExecutorExtension.class

    private var discoverMovieLiveData: MediatorLiveData<Resource<DiscoverMovieResponse>> = MediatorLiveData()
    
    fun observeDiscoverMovie(): LiveData<Resource<DiscoverMovieResponse>> {
            return discoverMovieLiveData
        }
    
    fun fetchDiscoverMovies(page: Int) {
    
            discoverMovieLiveData.value = Resource.loading(null) // this emit get observed immediately 
    
            val source = movieRepository.fetchDiscoverMovies(page)
            discoverMovieLiveData.addSource(source) {
                discoverMovieLiveData.value = it // never gets called
                discoverMovieLiveData.removeSource(source)
            }
        } 
    
    fun fetchDiscoverMovies(page: Int): LiveData<Resource<DiscoverMovieResponse>> {
            return LiveDataReactiveStreams.fromPublisher(
                apiService.fetchDiscoverMovies(page)
                    .subscribeOn(Schedulers.io())
                    .map { d ->
                        Resource.success(d) // never gets called in unit test
                    }
                    .onErrorReturn { e ->
                        Resource.error(ApiErrorHandler.getErrorByThrowable(e), null) // // never gets called in unit test
                    }
            )
        }
    
    class RxImmediateSchedulerRule : TestRule {
    
        private val immediate = object : Scheduler() {
            override fun createWorker(): Worker {
                return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
            }
        }
    
        override fun apply(base: Statement, description: Description): Statement {
            return object : Statement() {
                @Throws(Throwable::class)
                override fun evaluate() {
                    RxJavaPlugins.setInitIoSchedulerHandler { immediate }
                    RxJavaPlugins.setInitComputationSchedulerHandler { immediate }
                    RxJavaPlugins.setInitNewThreadSchedulerHandler { immediate }
                    RxJavaPlugins.setInitSingleSchedulerHandler { immediate }
                    RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate }
    
                    try {
                        base.evaluate()
                    } finally {
                        RxJavaPlugins.reset()
                        RxAndroidPlugins.reset()
                    }
                }
            }
        }
    
    }
    
    class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
    
        override fun beforeEach(context: ExtensionContext?) {
            ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
                override fun executeOnDiskIO(runnable: Runnable) {
                    runnable.run()
                }
    
                override fun postToMainThread(runnable: Runnable) {
                    runnable.run()
                }
    
                override fun isMainThread(): Boolean {
                    return true
                }
            })
        }
    
        override fun afterEach(context: ExtensionContext?) {
            ArchTaskExecutor.getInstance().setDelegate(null)
        }
    
    }
    

    指定
    RxImmediateSchedulerRule
    的方式对JUnit5不起作用。如果在
    apply()
    方法中放置断点,您将看到它没有被执行

    相反,您应该创建指定的扩展:

    然后在测试类的注释中应用
    TestSchedulerExtension

    
        @ExtendWith(value = [InstantExecutorExtension::class, TestSchedulerExtension::class])
        class MainViewModelTest {
    
            private val apiService: ApiService = mock()
            private lateinit var movieRepository: MovieRepository
            private lateinit var viewModel: MainViewModel
    
            @BeforeEach
            fun init() {
                movieRepository = MovieRepository(apiService)
                viewModel = MainViewModel(movieRepository)
            }
    
            @Test
            fun loadMovieListFromNetwork() {
                val mockResponse = DiscoverMovieResponse(1, emptyList(), 100, 10, 0, "", false)
                val call: Flowable = Flowable.just(mockResponse)
                whenever(apiService.fetchDiscoverMovies(1)).thenReturn(call)
    
                viewModel.fetchDiscoverMovies(1)
    
                assertEquals(Resource.success(mockResponse), LiveDataTestUtil.getValue(viewModel.discoverMovieLiveData))
            }
    
        }
    
    现在测试将通过。现在您已经进行了测试,该观察器已按预期值调度


    从另一个角度:这是单元测试吗?当然不是,因为在这个测试中,我们与两个单元交互:
    MainViewModel
    MovieRepository
    。这更符合“集成测试”的术语。如果您模拟了
    MoviesRepository
    ,那么这将是一个有效的单元测试:

    
    @ExtendWith(value = [InstantExecutorExtension::class, TestSchedulerExtension::class])
    class MainViewModelTest {
    
        private val movieRepository: MovieRepository = mock()
        private val viewModel = MainViewModel(movieRepository)
    
        @Test
        fun loadMovieListFromNetwork() {
            val mockResponse = DiscoverMovieResponse(1, emptyList(), 100, 10, 0, "", false)
            val liveData =
                MutableLiveData>().apply { value = Resource.success(mockResponse) }
            whenever(movieRepository.fetchDiscoverMovies(1)).thenReturn(liveData)
    
            viewModel.fetchDiscoverMovies(1)
    
            assertEquals(Resource.success(mockResponse), getValue(viewModel.discoverMovieLiveData))
        }
    
    }
    

    注意,
    MovieRepository
    应与
    fetchDiscoverMovies()
    一起声明为
    open
    ,以便能够模拟它。另外,你可以考虑使用插件。

    我认为你所需要做的只是改变< /P>
    val call: Flowable<DiscoverMovieResponse> = successCall(mockResponse)
    
    val调用:Flowable=successCall(mockResponse)
    

    val调用:Flowable=Flowable.just(mockResponse)
    
    使用来自架构组件google示例的类。因此,您需要将其复制/粘贴到您的项目中

    因此,在一天结束时,您的新测试将如下所示(假设所有关联和模拟都正确地设置在测试类的顶部)
    。此外,您正在使用一个InstantExecutoExtension,就像上面azizbekian向您展示的那样

    @Test
    fun loadMovieListFromNetwork() {
        val mockResponse = DiscoverMovieResponse(1, emptyList(), 100, 10)
        val call: Flowable<DiscoverMovieResponse> = Flowable.just(mockResponse)
        whenever(apiService.fetchDiscoverMovies(1)).thenReturn(call)
    
        viewModel.fetchDiscoverMovies(1)
    
        assertEquals(Resource.success(mockResponse), LiveDataTestUtil.getValue(viewModel.discoverMovieLiveData))
    }
    
    @测试
    乐趣加载MovieListFromNetwork(){
    val mockResponse=DiscoverMovieResponse(1,emptyList(),100,10)
    val调用:Flowable=Flowable.just(mockResponse)
    无论何时(apiService.fetchDiscoverMovies(1))。然后返回(调用)
    viewModel.FetchDiscoveryMovies(1)
    assertEquals(Resource.success(mockResponse)、LiveDataTestUtil.getValue(viewModel.discoverMovieLiveData))
    }
    

    如果该测试通过,则意味着您能够成功观察网络请求的结果并返回成功的响应。

    您可以添加
    liveData.observeForever
    的代码吗?与其使用ArgumentMatcher,不如使用ArgumentCaptor并比较Resource.loading返回的实际内容。在存储库中,io调度程序上订阅了apiService.FetchDiscoveryMovies(),这意味着Flowable不会在同一线程上同步发送通知。因此,请检查您是否正确配置了测试调度程序。@SanlokLee观察得很好。我更改了线程,但无法使LiveData与Rx一起工作。请检查我更新的问题。@second ObserveForver是androidx.Lifecycle中的内置方法。您能在github发布一个带有该设置的最小项目吗?我去看看。非常感谢。在这一点上,我很想知道1。在单元测试中,若我们模拟“movieRepository”,那个么将不会和apiService交互。可以吗?2.编写单元测试和交互测试的最佳实践是什么?我应该为每一个写两个测试函数吗?或者我应该将它们放在不同的目录中吗?1。单元测试是指单元测试。在您的例子中,您希望在
    ViewModel
    类中测试的单元,这意味着任何其他依赖项都应该被存根。在您的例子中,
    ViewModel
    甚至不知道
    ApiService
    类的存在,因此不与
    ApiService
    交互是完全可以的。与
    ApiService
    交互是错误的(如果您使用单元测试介绍
    ViewModel
    )。我不知道你所说的“互动”是什么意思。也许你的意思是“整合”。集成测试将意味着同时测试多个组件。例如,执行操作
    foo
    会在
    SharedPrefs
    中保存一个值,然后验证
    SharedPrefs
    实际上保留了该值。在您的例子中,有一个API层,这意味着您可以查看库。现在一切都清楚了。我应该把单元测试和集成类放在/test文件夹中吗?有什么惯例吗?这个答案太棒了,但是,我试过了,但它对我不起作用。我在beforeTestExecution(context:ExtensionContext?)方法中添加了:
    RxAndroidPlugins.setInitMainThreadSchedulerHandler{Schedulers.trampoline()}
    ,它现在正在工作@azizbekian指定的链接(zaglab.io)具有