1

在编写单元测试并试图弄清楚如何测试使用 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 中的一个属性,在运行测试时显然未初始化,我不知道如何解决这个问题。无法按照当前的方式测试我的视图模型吗?我需要更改什么才能对其运行测试?我将不胜感激任何指导或建议。

4

0 回答 0