gpt4 book ai didi

android - 使用 LiveData 的 JUnit5 测试不执行订阅者的回调

转载 作者:太空狗 更新时间:2023-10-29 16:25:36 24 4
gpt4 key购买 nike

背景:

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

Activity -> ViewModel -> Repository -> ApiService (Retrofit Interface)
  1. Activity 订阅一个LiveData并监听事件变化

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

  3. ViewModel 然后调用存储库从 ApiService 获取电影列表

  4. ApiService 返回 Resource.success(..)Resource.error(.LiveData。 .)

  5. ViewModel 然后将 ApiServiceLiveData 结果合并到 MediatorLiveData

我的查询:

在单元测试中,只有第一个发射 Resource.loading(..) 是由 ViewModel 中的 MediatorLiveData 生成的。 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)"
)
}

Resource 是一个通用的包装器类,用于包装不同场景的数据,例如加载、成功、错误。

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

编辑:#1

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

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

}

最佳答案

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

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


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:


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

}

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


换个角度:这是单元测试吗?当然不是,因为在这个测试中我们与 2 个单元交互:MainViewModelMovieRepository。这更符合“集成测试”的术语。如果您模拟了 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 以便能够模拟它。或者你可以考虑使用 kotlin-allopen插件。

关于android - 使用 LiveData 的 JUnit5 测试不执行订阅者的回调,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56422249/

24 4 0