I'm trying to run a unit test on my RecyclerView. For my first test, I want to see if the RecyclerView is displayed.
#RunWith(RobolectricTestRunner::class)
class WordListFragmentTest {
// Executes task sin the Architecture component in the same thread.
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var scenario: FragmentScenario<WordListFragment>
private lateinit var viewModel: MainViewModel
val word = Word("Word")
#Before
fun setup() {
viewModel = mock(MainViewModel::class.java)
scenario = launchFragment(
factory = MainFragmentFactory(viewModel),
fragmentArgs = null,
themeResId = R.style.Theme_Words,
initialState = Lifecycle.State.RESUMED
)
}
#Test
fun `recyclerView displayed`() {
onView(withId(R.id.recyclerView))
.check(matches(isDisplayed()))
}
After running the test I get the following error.
java.lang.Exception: Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(getMainLooper()).idle() call.
This appears to be related to LiveData observer that submits the list in the fragment. If I comment out the submit function the test will run.
The Fragment.
class WordListFragment(private val viewModel: MainViewModel) : Fragment() {
...
private fun submitList() {
viewModel.wordList.observe(viewLifecycleOwner, {
it?.let {
rvAdapter.submitList(it)
}
})
}
}
MianViewModel
class MainViewModel #Inject constructor(
var repository: IWordRepository,
#IoDispatcher var ioDispatcher: CoroutineDispatcher
) : ViewModel() {
var wordList: LiveData<List<Word>> = repository.allWords
...
}
This link states Robolectric will default to LooperMode.LEGACY behavior, but this can be overridden by applying a #LooperMode(NewMode) annotation to a test package, test class, or test method, or via the 'robolectric.looperMode' system property. I'm still experiencing the same error when I run my test.
Related
I'm trying to do ViewModel testing using Kotlin(1.6.21) Coroutines(1.6.4) and Kotlin Flow.
Following official Kotlin coroutine testing documentation but ViewModel is not waiting/returning a result for suspending functions before test completion. Have gone through top StackOverflow answers and tried all suggested solutions like injecting the same CoroutineDispatcher, and passing the same CoroutineScope but none worked so far. So here I am posting the current simple test implementation. Have to post all classes code involved in the test case to get a better idea.
ReferEarnDetailViewModel.kt:
Injected Usecase and CoroutineContextProvider and calling API using viewModelScope with provided dispatcher. But after calling callReferEarnDetails() from the test case, it is not collecting any data emitted by the mock use case method. Have tried with the direct repo method call, without Kotlin flow as well but same failure.
#HiltViewModel class
ReferEarnDetailViewModel #Inject constructor(
val appDatabase: AppDatabase?,
private val referEarnDetailsUseCase: ReferEarnDetailsUseCase,
private val coroutineContextProvider: CoroutineContextProvider) : BaseViewModel() {
fun callReferEarnDetails() {
setProgress(true)
viewModelScope.launch(coroutineContextProvider.default + handler) {
referEarnDetailsUseCase.execute(UrlUtils.getUrl(R.string.url_referral_detail))
.collect { referEarnDetail ->
parseReferEarnDetail(referEarnDetail)
}
}
}
private fun parseReferEarnDetail(referEarnDetail:
ResultState<CommonEntity.CommonResponse<ReferEarnDetailDomain>>) {
when (referEarnDetail) {
is ResultState.Success -> {
setProgress(false)
.....
}
}
}
ReferEarnCodeUseCase.kt: Returning Flow of Api response.
#ViewModelScoped
class ReferEarnCodeUseCase #Inject constructor(private val repository:
IReferEarnRepository) :BaseUseCase {
suspend fun execute(url: String):
Flow<ResultState<CommonEntity.CommonResponse<ReferralCodeDomain>>> {
return repository.getReferralCode(url)
}
}
CoroutineTestRule.kt
#ExperimentalCoroutinesApi
class CoroutineTestRule(val testDispatcher: TestDispatcher =
StandardTestDispatcher()) : TestWatcher() {
val testCoroutineDispatcher = object : CoroutineContextProvider {
override val io: CoroutineDispatcher
get() = testDispatcher
override val default: CoroutineDispatcher
get() = testDispatcher
override val main: CoroutineDispatcher
get() = testDispatcher
}
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
ReferEarnDetailViewModelTest.kt
#RunWith(JUnit4::class)
#ExperimentalCoroutinesApi
class ReferEarnDetailViewModelTest {
private lateinit var referEarnDetailViewModel: ReferEarnDetailViewModel
private lateinit var referEarnDetailsUseCase: ReferEarnDetailsUseCase
#get:Rule
val coroutineTestRule = CoroutineTestRule()
#Mock
lateinit var referEarnRepository: IReferEarnRepository
#Mock
lateinit var appDatabase: AppDatabase
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
referEarnDetailsUseCase = ReferEarnDetailsUseCase(referEarnRepository)
referEarnDetailViewModel = ReferEarnDetailViewModel(appDatabase,
referEarnDetailsUseCase , coroutineTestRule.testCoroutineDispatcher)
}
#Test
fun `test api response parsing`() = runTest {
val data = ResultState.Success( TestResponse() )
//When
Mockito.`when`(referEarnDetailsUseCase.execute("")).thenReturn(flowOf(data))
//Call ViewModel function which further call usecase function.
referEarnDetailViewModel.callReferEarnDetails()
//This should be false after API success response but failing here....
assertEquals(referEarnDetailViewModel.showProgress.get(),false)
}
}
Have tried this solution:
How test a ViewModel function that launch a viewModelScope coroutine? Android
Kotlin
Inject and determine CoroutineScope on ViewModel creation
As it is stated in the documentation runTest awaits completion of all the launched in its TestScope coroutines (or throws a timeout). But it does so on exit from the test body. In your case assertEquals fails inside the test body, so test fails immediately.
Generally speaking, this mechanism of awaiting completion of all jobs is a mean of preventing leaks and is not suitable for your purpose.
There are two ways to control the coroutines execution inside the test body:
Use methods to control virtual time. E.g. advanceUntilIdle should help in this case - use it before asserting the result and it will execute all the tasks scheduled on the given TestDispatcher.
Use regular ways to await execution, e.g. return a job and await its' completion before checking the result. This requires some code redesign, but this is a recommended approach. Check out a couple of paragraphs above the Setting the Main dispatcher chapter.
And then I'm seeing
[Test worker #coroutine#1] test
[Test worker #coroutine#2] viewModel
So this is giving me problems at the time to verify because it says that there's empty.
I'm using in my viewModel a CoroutineDispatcher injected with Hilt as
#HiltViewModel
class LocationsViewModel #Inject constructor(
private val locationsUseCase: LocationsUseCase,
#IODispatcher private val dispatcher: CoroutineDispatcher) : ViewModel() {
init { viewModelScope.launch(dispatcher) { locationsUseCase() }}
}
And the test I'm doing
private val testDispatcher = StandardTestDispatcher()
#Test
fun test() = runTest(testDispatcher){ ... }
fun createLocationsViewModel() = LocationsViewModel(locationsUseCase, testDispatcher)
Your viewmodel tries to initialise, but fails, because it doesn't know what to do with locationsUseCase call. Try mock it in setUp() method. With mockito-kotlin and mockito-core dependencies it would be something like
#Before
fun setUp() = runTest(testDispatcher) {
whenever(locationsUseCase.invoke()).thenReturn(Result.Success)
}
This code is not strict, because I don't know the rest of your code base. So think of it as a reference.
I am using Hilt for DI in my project. I am trying write unit test cases for LiveData object, but it's not coming under coverage.
ViewModel
#HiltViewModel
class HealthDiagnosticsViewModel #Inject constructor(
private var networkHelper: NetworkHelper
) : ViewModel() {
var result = MutableLiveData<Int>()
.....
}
My unit test class is as below:
HealthViewModelTest
#HiltAndroidTest
#RunWith(RobolectricTestRunner::class)
#Config(application = HiltTestApplication::class)
class HealthDiagnosticsViewModelTest{
#get:Rule
var hiltRule = HiltAndroidRule(this)
#Inject
lateinit var networkHelper: NetworkHelper
lateinit var healthDiagnosticsViewModel: HealthDiagnosticsViewModel
#Before
fun setUp() {
hiltRule.inject()
healthDiagnosticsViewModel = HealthDiagnosticsViewModel(networkHelper)
}
#Test
fun testGetResult() {
val result = healthDiagnosticsViewModel.result.value
Assert.assertEquals(null, result)
}
#Test
fun testSetResult() {
healthDiagnosticsViewModel.result.value = 1
Assert.assertEquals(1, healthDiagnosticsViewModel.result.value)
}
}
Test Cases are passed but it's not coming under method coverage.
I'll share with you the an example of my code that would solve your problem.
I'm usnig ViewModel with Dagger Hilt
You don't have to use Robelectric, you can use MockK library.
Replace your HiltRule with this Rule:
#get:Rule
var rule: TestRule = InstantTaskExecutorRule()
This is my ViewModel class
using MockK, you can mock the networkHelper class without Hilt.
So, your setup method will be like that:
lateinit var networkHelper: NetworkHelper
......
......
......
#Before
fun setUp() {
networkHelper = mockk<NetworkHelper>()
healthDiagnosticsViewModel = HealthDiagnosticsViewModel(networkHelper)
}
4)The most important part in your test is to Observe to the LiveData first.
#Test
fun testGetResult() {
healthDiagnosticsViewModel.result.observeForever {}
val result = healthDiagnosticsViewModel.result.value
Assert.assertEquals(null, result)
}
You can observe to the livedata for each unit test, but keep in mind to Observe first before change data.
I am new to testing and I am writing a test for my ViewModel. I have a couple of questions about it.
In my Test method, unless I add a delay(10), the test fails saying:
Wanted but not invoked:
breweryRepository.getBeerStyles();
-> at com.helenc.test.repositories.BreweryRepository.getBeerStyles(BreweryRepository.kt:12)
Actually, there were zero interactions with this mock.
Why do I need a delay? Is there a better way to do it?
I added the test in the test folder (not in androidTest). Is this correct? I am not sure since I am using some androidx dependencies, like androidx.lifecycle.Observer or androidx.arch.core.executor.testing.InstantTaskExecutorRule.
This is my ViewModel test file:
#RunWith(JUnit4::class)
class BeerStylesViewModelTest {
private lateinit var viewModel: BeerStylesViewModel
private lateinit var repository: BreweryRepository
private lateinit var beerStylesObserver: Observer<Resource<List<StylesData>>>
private val successResource = Resource.success(mockedBeerStylesList)
#Rule
#JvmField
val instantExecutorRule = InstantTaskExecutorRule()
#ObsoleteCoroutinesApi
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
#ObsoleteCoroutinesApi
#ExperimentalCoroutinesApi
#Before
fun setUp() {
Dispatchers.setMain(mainThreadSurrogate)
repository = mock()
runBlocking {
whenever(repository.getBeerStyles()).thenReturn(successResource)
}
viewModel = BeerStylesViewModel(repository)
beerStylesObserver = mock()
}
#Test
fun `when load beerStyles success`() = runBlocking {
viewModel.beerStyles.observeForever(beerStylesObserver)
delay(10)
verify(repository).getBeerStyles()
verify(beerStylesObserver).onChanged(Resource.loading(null))
verify(beerStylesObserver).onChanged(successResource)
}
#ObsoleteCoroutinesApi
#ExperimentalCoroutinesApi
#After
fun tearDown() {
Dispatchers.resetMain()
mainThreadSurrogate.close()
}
}
I clearly don't understand how to unit test business logic inside Transformation. In my specific case I need to test Transformations.map, but I guess Transformations.switchmap would be the same.
The following is just an example of my scenario, and what I'd like to achieve.
MyViewModel.kt
class MyViewModel: ViewModel() {
private val _sampleLiveDataIwannaTest : MutableLiveData<Int> = MutableLiveData()
val sampleLiveDataIWannaTest: Livedata<Int> = _sampleLiveDataIWannaTest
// ...
val liveDataImNotInterestedIn = Transformations.map(myRepository.streamingData){
streaming->
_sampleLiveDataIwannaTest.postValue(streaming.firstElementValue +streaming.lastElementValue)
streaming
}
// ...
}
With:
val liveDataImNotInteresedIn : LiveData<Foo>
myRepository.streamingData : LiveData<Foo>
myRepository.streamingData is a data source that wakes up the Transformations.map which, in turn, starts the business logic I'm interested in (the value posted in _sampleLiveDataIwannaTest). In this particular test, I don't care about anything else.
MyViewModelTest.kt
class MyViewModelTest {
#get:Rule val rule = InstantTaskExecutorRule()
#RelaxedMockK
lateinit var myRepository : MyRepository
#OverrideMockKs
lateinit var sut: MyViewModel
#Before
fun setUp() {
MockKAnnotations.init(this, relaxUnitFun = true)
}
#Test
fun Transformations_Test(){
sut.liveDataImNotInterestedIn.observeForever{}
// 1)I really don't know how to mock the livedata that returns from
// myRepository.streamingData . Something like this is correct?
// every{myRepository.streamingData}.returns{< LiveData of type Int > }
// 2) I wish to write this kind of test:
//
// assertEquals(5, sampleLiveDataIWannaTest.value)
}
I'm using MockK instead of Mockito.
The unit test code will look like this:
class MyViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#RelaxedMockK
lateinit var myRepository : MyRepository
#RelaxedMockK
lateinit var mockedSampleLiveDataIWannaTest : Observer<Int>
#OverrideMockKs
lateinit var sut: MyViewModel
#Before
fun setUp() {
MockKAnnotations.init(this, relaxUnitFun = true)
}
#Test
fun Transformations_Test(){
val expected = (*YOUR EXPECTED DATA HERE FROM REPOSITORY*)
every { myRepository.streamingData() } answers { expected }
sut.sampleLiveDataIWannaTest.observeForever(mockedSampleLiveDataIWannaTest)
verify { myRepository.streamingData() }
verify() { mockedSampleLiveDataIWannaTest.onChanged(Int) }
confirmVerified(myRepository, mockedSampleLiveDataIWannaTest)
}
if your repository is using coroutines then change every to coEvery and verify to coVerify
to learn more about MockK: https://mockk.io/