Search code examples
androidandroid-livedatarobolectricandroid-unit-testingandroid-fragmentscenario

Robolectric start a fragment that has an observer


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

  • implementing a shadow class for the Main Looper. Annotating the class with Looper mode.
@RunWith(RobolectricTestRunner::class)
@LooperMode(LooperMode.Mode.PAUSED)
class MyFragmentTest {
  • Adding scenario states
    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/


Solution

  • I took a look into your repository on github here. Here's what I've found.

    Problem 1

    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.

    Problem 2

    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()
        }
    

    Full Code

    @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()
            }
        }
    }