I'm struggling for a certain amount of time so I decided to ask for help here... I'm using almost the as same architecture as Google sample: GithubBrowserSample.
In the test of one of my fragment (androidTest) I encounter this error:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8191)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1420)
at android.view.View.requestLayout(View.java:24454)
at android.view.View.requestLayout(View.java:24454)
at android.view.View.requestLayout(View.java:24454)
at android.view.View.requestLayout(View.java:24454)
at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3028)
at android.view.View.requestLayout(View.java:24454)
at androidx.recyclerview.widget.RecyclerView.requestLayout(RecyclerView.java:4412)
at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.triggerUpdateProcessor(RecyclerView.java:5582)
at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeInserted(RecyclerView.java:5557)
at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeInserted(RecyclerView.java:12278)
at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemRangeInserted(RecyclerView.java:7498)
at androidx.recyclerview.widget.AdapterListUpdateCallback.onInserted(AdapterListUpdateCallback.java:42)
at androidx.recyclerview.widget.AsyncListDiffer.submitList(AsyncListDiffer.java:283)
at androidx.recyclerview.widget.AsyncListDiffer.submitList(AsyncListDiffer.java:231)
at androidx.recyclerview.widget.ListAdapter.submitList(ListAdapter.java:128)
at com.maximesarrato.lafayapp.ui.training.TrainingListFragment$onViewCreated$3.onChanged(TrainingListFragment.kt:112)
at com.maximesarrato.lafayapp.ui.training.TrainingListFragment$onViewCreated$3.onChanged(TrainingListFragment.kt:39)
at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:149)
at androidx.lifecycle.LiveData.setValue(LiveData.java:307)
at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
at androidx.lifecycle.LiveData$1.run(LiveData.java:91)
at androidx.arch.core.executor.testing.InstantTaskExecutorRule$1.postToMainThread(InstantTaskExecutorRule.java:43)
at androidx.arch.core.executor.ArchTaskExecutor.postToMainThread(ArchTaskExecutor.java:101)
at androidx.lifecycle.LiveData.postValue(LiveData.java:291)
at androidx.lifecycle.MutableLiveData.postValue(MutableLiveData.java:45)
at com.maximesarrato.lafayapp.ui.training.TrainingListFragmentTest.testTrainingListLoaded(TrainingListFragmentTest.kt:119)
at java.lang.reflect.Method.invoke(Native Method)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at androidx.test.internal.runner.junit4.statement.RunBefores.evaluate(RunBefores.java:80)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at androidx.test.ext.junit.runners.AndroidJUnit4.run(AndroidJUnit4.java:104)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:56)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:392)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2189)
This error occurs on this part of the code, when I submit data to my ListAdapter:
viewModel.trainingsLiveData.observe(viewLifecycleOwner, Observer { result ->
result?.data?.let {
// Error occurs here
adapter.submitList(it)
}
})
This error occurs only when the list is not empty... I didn't encountered any errors when using the application but only during tests.
Here is the source code of my test:
@RunWith(AndroidJUnit4::class)
class TrainingListFragmentTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Rule
@JvmField
val executorRule = TaskExecutorWithIdlingResourceRule()
@Rule
@JvmField
val countingAppExecutors = CountingAppExecutorsRule()
@Rule
@JvmField
val dataBindingIdlingResourceRule = DataBindingIdlingResourceRule()
private val navController = mock<NavController>()
private val trainingsLiveData = MutableLiveData<Resource<List<Training>>>()
private val isPremiumLiveData = MutableLiveData<PremiumAccount>()
private val trainingDeletedLiveData = MutableLiveData<Event<Boolean>>()
private val navigateToCreateTrainingLiveData = MutableLiveData<Event<Boolean>>()
private val navigateToExerciseListLiveData = MutableLiveData<Event<Training>>()
private lateinit var viewModel: TrainingListViewModel
@Before
fun init() {
viewModel = mock(TrainingListViewModel::class.java)
val trainingListFragment = TrainingListFragment()
trainingListFragment.viewModelFactory = ViewModelUtil.createFor(viewModel)
trainingListFragment.appExecutors = countingAppExecutors.appExecutors
trainingListFragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
if (viewLifecycleOwner != null) {
Navigation.setViewNavController(trainingListFragment.requireView(), navController)
}
}
`when`(viewModel.trainingsLiveData).thenReturn(trainingsLiveData)
`when`(viewModel.isPremium).thenReturn(isPremiumLiveData)
`when`(viewModel.trainingDeleted).thenReturn(trainingDeletedLiveData)
`when`(viewModel.navigateToCreateTraining).thenReturn(navigateToCreateTrainingLiveData)
`when`(viewModel.navigateToExerciseList).thenReturn(navigateToExerciseListLiveData)
val scenario = launchFragmentInContainer(themeResId = R.style.Theme_LafayWorkbook) {
trainingListFragment
}
dataBindingIdlingResourceRule.monitorFragment(scenario)
scenario.onFragment { fragment ->
Navigation.setViewNavController(fragment.requireView(), navController)
fragment.binding.trainingListRv.itemAnimator = null
fragment.disableProgressBarAnimations()
}
}
/**
* Verify that when data is loading then Progress bar is displayed
* and Helper text is not displayed.
*
*/
@Test
// Works
fun testLoading() {
trainingsLiveData.postValue(Resource.loading(null))
onView(withId(R.id.trainingProgressBar)).check(matches(isDisplayed()))
onView(withId(R.id.trainingHelperTextView)).check(matches(not(isDisplayed())))
}
/**
* Verify that when data is loaded and correspond to an empty list then Progress
* bar is not displayed and Helper text is displayed.
*
*/
@Test
// Works
fun testEmptyListLoaded() {
trainingsLiveData.postValue(Resource.success(listOf()))
onView(withId(R.id.trainingProgressBar)).check(matches(not(isDisplayed())))
onView(withId(R.id.trainingHelperTextView)).check(matches(isDisplayed()))
}
/**
* Checks helper text disappears and list of Training is correctly displayed
*
*/
@Test
// This test doesn't work :(
fun testTrainingListLoaded() {
val trainings = createTrainings(
3,
createExercises(2)
)
trainingsLiveData.postValue(Resource.success(trainings.toMutableList()))
onView(withId(R.id.trainingHelperTextView)).check(matches(not(isDisplayed())))
onView(listMatcher().atPosition(0)).check(matches(hasDescendant(withText("Level 1"))))
}
}
Do you guys have an idea about why this is happening and how I can fix it?
Because you are setting the value directly. Use postValue
like the other tests:
fun testTrainingListLoaded() {
val trainings = createTrainings(
3,
createExercises(2)
)
trainingsLiveData.postValue(Resource.success(trainings.toMutableList()))
onView(withId(R.id.trainingHelperTextView)).check(matches(not(isDisplayed())))
onView(listMatcher().atPosition(0)).check(matches(hasDescendant(withText("Level 1"))))
}
To see what is the difference between postValue
and setValue
you can check this.
To summarize, the key difference would be:
setValue() method must be called from the main thread. But if you need set a value from a background thread, postValue() should be used.