I am a complete beginner in writing unit tests and trying to figure out how to test a ViewModel that makes use of the Giphy API.
This is my 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
}
}
This is my Test:
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)
}
}
This is 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()
}
}
}
}
}
The first test is passing. When I run the second test, I get this:
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 is a property in GPHContent that is apparently uninitialized at the time of running the test, I'm not sure how to solve this. Is it not possible to test my viewmodel the way it currently is? What do I need to change to be able to run tests on it? I would appreciate any guidance or advice.