Фрагментное тестирование: только исходный поток, создавший иерархию представлений, может касаться его представлений - PullRequest
0 голосов
/ 23 апреля 2020

Я борюсь за определенное время, поэтому я решил попросить о помощи здесь ... Я использую почти ту же архитектуру, что и образец 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$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)

Эта ошибка возникает в этой части кода, когда я отправляю данные в свой 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"))))
    }
}

Ребята, у вас есть представление о том, почему это происходит и как я могу это исправить?

Ответы [ 2 ]

0 голосов
/ 30 апреля 2020

Эта проблема связана с: FragmentScenario не работает должным образом и был успешно решен!

Я не должен был использовать правило IntantTaskExecutorRule:

@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()

И действительно, без этого правила я должен был бы использовать postValue вместо setValue, как сказал Мухаммед. Кроме того, как уже упоминалось в другом вопросе, который я задал и связал с этим сообщением, основная проблема была связана с затенением переменных: { ссылка }

Спасибо за вашу помощь!

0 голосов
/ 24 апреля 2020

Потому что вы устанавливаете значение напрямую. Используйте 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"))))
    }

Чтобы увидеть разницу между postValue и setValue, вы можете проверить это .

Подводя итог, можно сказать, что ключевое отличие будет следующим:

Метод setValue () должен вызываться из основного потока. Но если вам нужно установить значение из фонового потока, следует использовать postValue ().

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...