Reputation: 1382
How to start a fragment with a LiveData observer in the test scope with Robolectric
Fragment
class MyFragment(private val viewModel: MyViewModel) : Fragment() {
...
fun myObserver {
...
// If I remove this observer the test will pass.
viewModel.MyLiveData.observe(viewLifecycleOwner, Observer{
...
}
}
}
My Test uses the RobolectricTestRunner so I can launch the fragment in the test scope.
@RunWith(robolectricTestRunner::class)
class MyFragmentTest {
// Executes tasks in the Architecture Components in the same thread
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun testOne() {
val viewModel: MyViewModel = mock(MyViewModel::class.java)
val scenario = launchFragmentInContainer(
factory = MainFragmentFactory(viewModel),
fragmentArgs = null
themeResId = R.style.Theme_MyTheme
)
// Tried implementing shadowOf as the error suggests.
}
}
I get the following error when trying to run the test. I've tried setting Main looper to idle before and after instantiating the FragmentScenario.
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.
I've tried the following
@RunWith(RobolectricTestRunner::class)
@LooperMode(LooperMode.Mode.PAUSED)
class MyFragmentTest {
scenario.moveToState(Lifecycle.State.CREATED)
scenario.moveToState(Lifecycle.State.RESUMED)
My Test dependencies.
// Test
testImplementation 'androidx.arch.core:core-testing:2.1.0'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3'
testImplementation "androidx.test.ext:junit-ktx:1.1.2"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3"
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'androidx.test.espresso:espresso-core:3.3.0'
testImplementation "org.robolectric:robolectric:4.5.1"
testImplementation "org.mockito:mockito-android:2.28.2"
// Testing Fragments
debugImplementation "androidx.fragment:fragment-testing:1.3.2"
Links I've used to find a solution' Testing LiveData Transformations? https://jeroenmols.com/blog/2019/01/17/livedatajunit5/
Upvotes: 1
Views: 2041
Reputation: 14660
I took a look into your repository on github here. Here's what I've found.
Your first problem is that you mock out a ViewModel
. So, when you simulate onResume
for your Fragment
it invokes:
fun liveDataObserver() {
viewModel.scoreLiveData.observe(viewLifecycleOwner, {
//
} )
}
Since viewModel
is mocked, scoreLiveData
is null
and you get an NPE.
To fix this, you also mock out scoreLiveData
method so that it returns some acceptable result:
...
val liveData = MutableLiveData<Int>().apply { value = 3 }
val viewModel = mock(MyViewModel::class.java)
doReturn(liveData).`when`(viewModel).scoreLiveData
...
This will fix your testOne
completely, but not yet testTwo
.
It's related only to your testTwo
method. The problem is that you're calling liveDataObserver()
in your also
block, and that is invoked before your Fragment's
viewLifecycleOwner
has been set in onCreateView
:
...
scenario = launchFragmentInContainer {
MyFragment(viewModel).also {
it.liveDataObserver()
}
}
...
I'm not sure what exactly you're trying to test here, but if you want to verify that you can start observing after Fragment's
View
has been created, you could do something as following:
...
// make sure your Fragment is started
scenario = launchFragmentInContainer (
factory = MainFragmentFactory(viewModel),
initialState = Lifecycle.State.STARTED
)
// call liveDataObserver on it
scenario.withFragment {
this.liveDataObserver()
}
@RunWith(RobolectricTestRunner::class)
class MyFragmentTest {
private lateinit var scenario: FragmentScenario<MyFragment>
@Test
fun testOne() = runBlockingTest {
val liveData = MutableLiveData<Int>().apply { value = 1 }
val viewModel = mock(MyViewModel::class.java)
doReturn(liveData).`when`(viewModel).scoreLiveData
scenario = launchFragmentInContainer(
factory = MainFragmentFactory(viewModel),
fragmentArgs = null,
themeResId = R.style.Theme_TDDScoreKeeper,
initialState = Lifecycle.State.STARTED
)
scenario.moveToState(Lifecycle.State.RESUMED)
scenario.recreate() // Simulates if the phone ran low on resources and the app had to be recreated.
}
@Test
fun testTwo() {
val liveData = MutableLiveData<Int>().apply { value = 1 }
val viewModel = mock(MyViewModel::class.java)
doReturn(liveData).`when`(viewModel).scoreLiveData
scenario = launchFragmentInContainer(
factory = MainFragmentFactory(viewModel),
initialState = Lifecycle.State.STARTED
)
scenario.withFragment {
this.liveDataObserver()
}
}
}
Upvotes: 2