背景:
我有一个简单的应用程序,它使用rests API调用来获取电影列表。项目结构如下:
Activity -> ViewModel -> Repository -> ApiService (Retrofit Interface)
该活动订阅 LiveData 并监听事件更改
ViewModel 托管活动观察到的 MediatorLiveData 。最初, ViewModel 在 MediatorLiveData 中设置一个Resource.loading(..)
值。
ViewModel 然后调用存储库以从 ApiService
ApiService 返回Resource.success(..)
或Resource.error(..)
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)
}
}
答案 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))
}
如果该测试通过,则表明您能够成功观察到网络请求的结果并返回成功的响应。