片段测试:只有创建视图层次结构的原始线程才能触及它的视图

Fragment testing: Only the original thread that created a view hierarchy can touch its views

我挣扎了一段时间,所以我决定在这里寻求帮助... 我使用的架构几乎与 Google 示例相同:GithubBrowserSample.

在我的一个片段(androidTest)的测试中我遇到了这个错误:

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.onChanged(TrainingListFragment.kt:112)
at com.maximesarrato.lafayapp.ui.training.TrainingListFragment$onViewCreated.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.run(LiveData.java:91)
at androidx.arch.core.executor.testing.InstantTaskExecutorRule.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.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.evaluate(TestWatcher.java:55)
at org.junit.rules.TestWatcher.evaluate(TestWatcher.java:55)
at org.junit.rules.TestWatcher.evaluate(TestWatcher.java:55)
at org.junit.rules.TestWatcher.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.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access[=10=]0(ParentRunner.java:58)
at org.junit.runners.ParentRunner.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.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access[=10=]0(ParentRunner.java:58)
at org.junit.runners.ParentRunner.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)

当我向我的 ListAdapter 提交数据时,这部分代码出现此错误:

viewModel.trainingsLiveData.observe(viewLifecycleOwner, Observer { result ->
    result?.data?.let {
        // Error occurs here
        adapter.submitList(it)
    }
})

仅当列表不为空时才会出现此错误...我在使用该应用程序时没有遇到任何错误,但仅在测试期间遇到。

这是我测试的源代码:

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

你们知道为什么会这样吗?我该如何解决?

因为你是直接设置值。像其他测试一样使用 postValue

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

要查看 postValuesetValue 之间的区别,您可以查看

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.

此问题已链接到: 并已成功解决!

我不应该使用规则 IntantTaskExecutorRule:

@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()

事实上,如果没有这条规则,我应该使用 postValue 而不是 Mohammad 所说的 setValue。 此外,正如我在另一个问题中提到的并链接到此 post 主要问题与变量阴影有关:

感谢您的帮助!