使用LiveData进行的JUnit5测试不会执行订阅者的回调

时间:2019-06-03 06:43:04

标签: android junit mockito android-livedata android-viewmodel

背景

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

Activity -> ViewModel -> Repository -> ApiService (Retrofit Interface)
  1. 该活动订阅 LiveData 并监听事件更改

  2. ViewModel 托管活动观察到的 MediatorLiveData 。最初, ViewModel MediatorLiveData 中设置一个Resource.loading(..)值。

  3. ViewModel 然后调用存储库以从 ApiService

  4. 获取电影列表
  5. ApiService 返回Resource.success(..)Resource.error(..)

  6. LiveData
  7. ViewModel 然后合并 MediatorLiveData

    ApiService LiveData 结果

我的查询

在单元测试中, MediatorLiveData 仅从 ViewModel 进行第一个发射Resource.loading(..) 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)
        }
    } 

Repository.class

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
                }
        )
    }

单元测试

@Test
fun loadMovieListFromNetwork() {
        val mockResponse = DiscoverMovieResponse(1, emptyList(), 100, 10)
        val call: Flowable<DiscoverMovieResponse> = successCall(mockResponse) // wraps the retrofit result inside a Flowable<DiscoverMovieResponse>
        whenever(apiService.fetchDiscoverMovies(1)).thenReturn(call)

        viewModel.fetchDiscoverMovies(1)

        verify(apiService).fetchDiscoverMovies(1)
        verifyNoMoreInteractions(apiService)

        val liveData = viewModel.observeDiscoverMovie()
        val observer: Observer<Resource<DiscoverMovieResponse>> = mock()
        liveData.observeForever(observer)

        verify(observer).onChanged(
            Resource.success(mockResponse) // TEST FAILS HERE AND GETS "Resource.loading(null)" 
        )
    }

资源是一个通用包装类,用于包装不同情况下的数据,例如加载,成功,错误。

class Resource<out T>(val status: Status, val data: T?, val message: String?) {
.......
}

编辑:#1

出于测试目的,我更新了存储库中的rx线程以在主线程上运行它。最后以 Looper not模拟异常结束。

fun fetchDiscoverMovies(page: Int): LiveData<Resource<DiscoverMovieResponse>> {
            return LiveDataReactiveStreams.fromPublisher(
                apiService.fetchDiscoverMovies(page)
                    .subscribeOn(AndroidSchedulers.mainThread())
                    .map {...}
                    .onErrorReturn {...}
            )
        }

在测试课中,

@ExtendWith(InstantExecutorExtension::class)
class MainViewModelTest {

    companion object {
        @ClassRule
        @JvmField
        val schedulers = RxImmediateSchedulerRule()
    }

    @Test
        fun loadMovieListFromNetwork() {
        .....  
       }
}

}

RxImmediateSchedulerRule.class

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()
                }
            }
        }
    }

}

InstantExecutorExtension.class

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)
    }

}

2 个答案:

答案 0 :(得分:2)

您指定--------------------------------------------------------------------------- UnboundLocalError Traceback (most recent call last) <ipython-input-12-360fef19693f> in <module> 1 #training the model 2 model_scratch = train(5, loaders_scratch, model_scratch, optimizer_scratch, ----> 3 criterion_scratch) <ipython-input-11-c90fddb93f0d> in train(n_epochs, loaders, model, optimizer, criterion) 9 #train model 10 model.train() ---> 11 for batch_idx, (data,target) in enumerate(loaders['train']): 12 13 # zero the parameter (weight) gradients UnboundLocalError: local variable 'photoshop' referenced before assignment 的方式不适用于JUnit5。如果您在RxImmediateSchedulerRule方法中放置一个断点,则会看到它没有被执行。

相反,您应该按照指定的here创建扩展名:

apply()

然后将 class TestSchedulerExtension : BeforeTestExecutionCallback, AfterTestExecutionCallback { override fun beforeTestExecution(context: ExtensionContext?) { RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() } RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() } RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() } } override fun afterTestExecution(context: ExtensionContext?) { RxJavaPlugins.reset() RxAndroidPlugins.reset() } } 应用于测试类的注释中,例如:

TestSchedulerExtension

现在测试将通过。现在您已经测试了,该观察者已经被分配了期望值。


从另一个角度来看:这是单元测试吗?可以肯定的是,因为在此测试中我们正在与2个单元进行交互: @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一起声明为open,以便能够对其进行模拟。另外,您可以考虑使用kotlin-allopen插件。

答案 1 :(得分:2)

我认为您要做的就是改变

val call: Flowable<DiscoverMovieResponse> = successCall(mockResponse)

val call: Flowable<DiscoverMovieResponse> = Flowable.just(mockResponse)

并且使用Google架构组件示例中的LiveDataUtil。因此,您需要将其复制/粘贴到您的项目中。

因此,最终,您的新测试将如下所示((假设在测试类的顶部正确设置了所有关联和模拟))。另外,您正在使用 InstantExecutorExtension ,就像上面的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))
}

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