4

I'm testing MyFragment using MockK. The fragment registers an Observer for a public, mutable LiveData in the onViewCreated method. The Observer calls a method within a mocked ViewModel. However (based on reading log output, in the unit test, verify appears to run before the Observer can begin its lambda.

My unit test:

class Tests {
    @get:Rule val rule: TestRule = InstantTaskExecutorRule()
    //...
    @Test
    fun updates_vm_with_editingListener() {
        /* Given */
        val listener = MutableLiveData(false)
        fragment.editingListener = listener
        mActivity.setFragment(fragment) // <-- this attaches the fragment to a dummy activity

        /* When */
        listener.value = true

        /* Then */
        Log.d("MyFragmentTest", "running verify")
        verify { vm.instruct(ReadWriteObj) } // vm is a mockk'ed class
    }
}

MyFragment contains this code:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        editingListener.observe(this, Observer {
            Log.d(TAG, "editing changed to $it")
            when(it) {
                true -> mViewModel.instruct(ReadWriteObj)
                false -> mViewModel.instruct(ReadOnlyObj)
            }
            Log.d(TAG, "editing messaged mViewModel")
        })
    }

The relevant log output is:

I/TestRunner: started: updates_vm_with_editingListener(FragmentTest)
D/FragmentTest: running verify
D/MyFragment: editing changed to true
D/MyFragment: editing messaged mViewModel
E/TestRunner: failed: updates_vm_with_editingListener(FragmentTest)
E/TestRunner: java.lang.AssertionError: Verification failed: call 1 of 1: MyViewModel(vm#1).instruct(eq(ReadWriteObj@2da49cd))). Only one matching call to LiftGroupCardViewModel(vm#1)/instruct(LiftGroupCardIntent) happened, but arguments are not matching:

And the matched obj with different arguments is a call made in the fragment's init block and isn't under test here.

I don't understand. The InstantTaskExecutorRule in my class should make the LiveData observer trigger immediately when the value is changed rather than in the next cycle/loop thing.

Edit

Shouldn't what actually happens be:

  1. mock viewmodel created and injected into fragment
  2. dummy livedata (listener) injected into fragment
  3. test begins
  4. fragment attached to activity
  5. fragment begins observing listener
  6. listener value changes
  7. fragment's observer fires, calling viewmodel.instruct(ReadWriteObj)
  8. verify confirms viewmodel.instruct was called with ReadWriteObj

Edit 2

I know I could insert Thread.sleep(1000) in my unit test to fix this, but that is a really funky code smell. Right now, there is about 500ms between the verify and the observer behavior, and that's on a laptop from 2013. If this unit test were run, in theory, on some slower machine, the sleep might not fix the problem at all.

user1713450
  • 1,307
  • 7
  • 18

0 Answers0