Espresso UI Testing, Cannot check loading state, How to test UI with live data and view model?

Viewed 570

Currently i am implementation UI test/ Instrumentation Test with Espresso, to test FragmentMovie at loading state, and success state. Due to multiple test library and other stuff,my test Instrumentation must contains hilt ijection.

UI testing scenario :

  1. When loading state, Check my loading progressbar and text loading is displayed
  2. When succes state, hide loading, and show recycle view
  3. Done

Problem

Loading state is skiped when running test. @Test will run after ViewModel postValue Success state. So, I can't test while loading state.

when i not write onView(withId(R.id.progressBar)).check(matches(isDisplayed())) the test is passed, but i want to test while loading state.

Possible fix, add delay(10000) to my view model, that will extend duration of loading state, but, my test still waiting success state.

Currently i am not use IdlingResource, because that not fix my problem,

Actual scenario:

  1. Fragment Run,
  2. ViewModel fetchdata,
  3. ViewModel loading,
  4. ViewModel success,
  5. FragmentTest run,
  6. viewAssert failed,

Expected :

  1. Fragment Run,
  2. ViewModel fetchdata,
  3. ViewModel loading,
  4. FragmentTest check progressbar,
  5. ViewModel success,
  6. FragmentTest check recycleView,
  7. done

Why? and How?

Code

Observer from fragmentMovie


 private val viewModel: MovieViewModel by viewModels()

   [...]

 viewModel.movie.observe(viewLifecycleOwner, {
            Log.d(TAG, "viewModelgetMovie: ${it.status}")
            when (it.status) {
                Status.LOADING -> {
                    Toast.makeText(requireContext(), "loading..", Toast.LENGTH_SHORT).show()
                }
                Status.SUCCESS -> {
                    binding.progressBar.visibility = View.GONE
                    binding.txLoading.visibility = View.GONE
                    if (it.data != null) {
                        list.addAll(it.data)
                        movieAdapter.notifyDataSetChanged()
                    }
                }
                Status.ERROR -> {
                    binding.progressBar.visibility = View.VISIBLE
                    binding.txLoading.text = it.message
                    Toast.makeText(requireContext(), "${it.message}", Toast.LENGTH_SHORT).show()
                }
            }
        })

viewmodel

@HiltViewModel
class MovieViewModel @Inject constructor(
    private val repository: MovieRepository,
    private val dispatcher: CoroutineDispatcher
) : ViewModel() {

    // new code
    private val moviePost = MutableLiveData<ResourceHelper<ArrayList<MovieData>>>()

    val movie: LiveData<ResourceHelper<ArrayList<MovieData>>>
        get() = moviePost

    init {
        fetchMovie()
    }

    private fun fetchMovie() {
        viewModelScope.launch(dispatcher) {
            moviePost.postValue(ResourceHelper.loading(null))
            repository.getMovie(1).let { movie ->
                moviePost.postValue(movie)
            }
        }
    }


}

MovieFragmentTest

package com.unlink.moviecatalogue6.ui.movie

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.unlink.moviecatalogue6.MainActivity
import com.unlink.moviecatalogue6.R
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@LargeTest
class MovieFragmentTest {
    @get:Rule(order = 0)
    var hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    var activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Before
    fun setUp() {
        hiltRule.inject()
    }

    @After
    fun tearDown() {

    }

    @Test
    fun happyPath() {
        onView(withId(R.id.progressBar)).check(matches(isDisplayed())) // error
        // how can i test on loading?
        onView(withId(R.id.recycleMovie)).check(matches(isDisplayed()))
    }
}

Error

androidx.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: 'is displayed on the screen to the user' doesn't match the selected view.
Expected: is displayed on the screen to the user
Got: "ProgressBar{id=2131231016, res-name=progressBar, visibility=GONE, width=1080, height=44, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=true, is-selected=false, layout-params=androidx.constraintlayout.widget.ConstraintLayout$LayoutParams@a455ee2, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}"

The error shown because my progress bar has gone. Because my test check the progress bar after success state.

Screenshoot

Loading state, but my test not run @Test

loading state

Success state, then @Test start

success state

that cause error view is gone

error

Any response will very appreciated

1 Answers

I think one way to do this is to pause dispatcher with testDispatcher inside your test before loading then resume it manually.

private val testDispatcher = TestCoroutineDispatcher()

@Test
fun happyPath() {
    testDispatcher.pauseDispather()
    onView(withId(R.id.progressBar)).check(matches(isDisplayed()))
    testDispatcher.resumeDispather()
    onView(withId(R.id.recycleMovie)).check(matches(isDisplayed()))
}

One note you should move your loading outside of viewmodel scope.

   moviePost.setValue(ResourceHelper.loading(null))
   viewModelScope.launch(dispatcher) {
        
        repository.getMovie(1).let { movie ->
            moviePost.postValue(movie)
        }
    }

You can read more about this here: https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey#4

Related