在编写单元测试并试图弄清楚如何测试使用 Giphy API 的 ViewModel 方面,我是一个完整的初学者。
这是我的视图模型:
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.ViewModel
import com.example.android.myproject.api.Gif
import com.example.android.myproject.api.Result
import com.example.android.myproject.repository.GifRepository
import com.giphy.sdk.core.models.Media
import com.giphy.sdk.core.models.enums.MediaType
import com.giphy.sdk.ui.pagination.GPHContent
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import java.util.concurrent.TimeUnit
class GifViewModel @ViewModelInject constructor(
private val gifRepository: GifRepository
) : ViewModel() {
private val isSearchingBehaviorSubject = BehaviorSubject.createDefault(false)
val isSearchingObservable: Observable<Boolean> =
isSearchingBehaviorSubject.observeOn(AndroidSchedulers.mainThread())
private val searchResultsBehaviorSubject = BehaviorSubject.create<GPHContent>()
val searchResultsObservable: Observable<GPHContent> =
searchResultsBehaviorSubject
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
private lateinit var selectedGif: Media
val randomGifObservable: Observable<Result<Gif, String>> =
Observable.interval(0, 10, TimeUnit.SECONDS, Schedulers.io())
.flatMap<Result<Gif, String>> {
gifRepository.getRandomGif()
.toObservable()
.map { Result.Success(it) }
}
.onErrorReturn { Result.Failure("Error getting a random GIF.") }
.observeOn(AndroidSchedulers.mainThread())
fun searchStateChanged(isSearching: Boolean) {
isSearchingBehaviorSubject.onNext(isSearching)
// this is needed to "reset" the results from a previous search
if (isSearching)
searchResultsBehaviorSubject.onNext(GPHContent.trendingGifs)
}
fun gifSearchQueryChanged(searchQuery: String) {
val results = if (searchQuery.length >= 2)
GPHContent.searchQuery(search = searchQuery, mediaType = MediaType.gif)
else
GPHContent.trendingGifs
searchResultsBehaviorSubject.onNext(results)
}
fun gifSelected(media: Media) {
selectedGif = media
}
fun selectedGif(): Media {
return selectedGif
}
}
这是我的测试:
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.example.android.myproject.RxImmediateSchedulerRule
import com.example.android.myproject.repository.GifRepository
import com.example.android.myproject.ui.main.GifViewModel
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
class GifViewModelTest {
@Rule
@JvmField
var testSchedulerRule = RxImmediateSchedulerRule()
@Rule
@JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var viewModel: GifViewModel
@Mock
private lateinit var gifRepository: GifRepository
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
viewModel = spy(GifViewModel(gifRepository))
}
@Test
fun `initial isSearching value is false`() {
val isSearching = viewModel.isSearchingObservable.blockingFirst()
assertFalse(isSearching)
}
@Test
fun `changing search state to true makes isSearching true`() {
viewModel.searchStateChanged(isSearching = true)
val isSearching = viewModel.isSearchingObservable.blockingSingle()
assertTrue(isSearching)
}
}
这是 RxImmediateSchedulerRule:
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.Schedulers
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
class RxImmediateSchedulerRule : TestRule {
override fun apply(base: Statement, description: Description?): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setSingleSchedulerHandler { Schedulers.trampoline() }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
try {
base.evaluate()
} finally {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
}
}
}
第一个测试通过了。当我运行第二个测试时,我得到了这个:
java.lang.ExceptionInInitializerError
at com.example.android.myproject.ui.main.GifViewModel.searchStateChanged(GifViewModel.kt:44)
at com.example.android.myproject.viewmodel.GifViewModelTest.changing search state to true makes isSearching true(GifViewModelTest.kt:49)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.mockito.internal.runners.DefaultInternalRunner$1$1.evaluate(DefaultInternalRunner.java:46)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:61)
at com.example.android.myproject.RxImmediateSchedulerRule$apply$1.evaluate(RxImmediateSchedulerRule.kt:22)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:77)
at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:83)
at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:39)
at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:163)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property apiClient has not been initialized
at com.giphy.sdk.core.a.b()
at com.giphy.sdk.ui.pagination.GPHContent.<init>()
at com.giphy.sdk.ui.pagination.GPHContent.<clinit>()
at com.example.android.myproject.ui.main.GifViewModel.searchStateChanged(GifViewModel.kt:48)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mockito.internal.creation.bytebuddy.MockMethodAdvice.tryInvoke(MockMethodAdvice.java:213)
at org.mockito.internal.creation.bytebuddy.MockMethodAdvice.access$400(MockMethodAdvice.java:35)
at org.mockito.internal.creation.bytebuddy.MockMethodAdvice$RealMethodCall.invoke(MockMethodAdvice.java:165)
at org.mockito.internal.invocation.InterceptedInvocation.callRealMethod(InterceptedInvocation.java:152)
at org.mockito.internal.stubbing.answers.CallsRealMethods.answer(CallsRealMethods.java:44)
at org.mockito.Answers.answer(Answers.java:100)
at org.mockito.internal.handler.MockHandlerImpl.handle(MockHandlerImpl.java:103)
at org.mockito.internal.handler.NullResultGuardian.handle(NullResultGuardian.java:29)
at org.mockito.internal.handler.InvocationNotifierHandler.handle(InvocationNotifierHandler.java:35)
at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.doIntercept(MockMethodInterceptor.java:61)
at org.mockito.internal.creation.bytebuddy.MockMethodAdvice.handle(MockMethodAdvice.java:106)
... 35 more
apiClient 是 GPHContent 中的一个属性,在运行测试时显然未初始化,我不知道如何解决这个问题。无法按照当前的方式测试我的视图模型吗?我需要更改什么才能对其运行测试?我将不胜感激任何指导或建议。