Тестирование Android Espresso SwipeRefreshLayout onRefresh не было запущено при прокрутке вниз

#android #integration-testing #android-espresso #robolectric #swiperefreshlayout

#Android #интеграция-тестирование #android-espresso #robolectric #swiperefreshlayout

Вопрос:

Я пытаюсь написать простой тест для извлечения для обновления в рамках интеграционного тестирования. Я использую новейшие компоненты для тестирования AndroidX и Robolectric. Я тестирую изолированный фрагмент, в который я вставляю mocked presenter.

Часть макета XML

 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@ id/refreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@ id/recyclerTasks"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
  

Фрагментная часть

 binding.refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                presenter.onRefresh();
            }
        });
  

Тест:

 onView(withId(R.id.refreshLayout)).perform(swipeDown());
verify(presenter).onRefresh();
  

но тест не проходит, сообщение:

Требуется, но не вызывается: presenter.onRefresh();

Приложение работает отлично, и для обновления вызывается presenter.onRefresh(). Я также выполнил отладку теста, и setOnRefreshListener был вызван, и это не значение null. Если я проведу тестирование с помощью пользовательского сопоставления, чтобы проверить состояние тестовых проходов SwipeRefreshLayout.

 onView(withId(R.id.refreshLayout)).check(matches(isRefreshing()));
  

Ответ №1:

Я провел небольшое расследование в прошлые выходные, так как столкнулся с той же проблемой, и это беспокоило меня. Я также провел некоторое сравнение с тем, что происходит на устройстве, чтобы определить различия.

Внутри androidx.swiperefreshlayout.widget.SwipeRefreshLayout есть mRefreshListener , который будет запускаться при onAnimationEnd вызове. AnimationEnd Будет запущен метод then OnRefreshListener.onRefresh .

Этот прослушиватель анимации ( mRefreshListener ) передается mCircleView (CircleImageView) и вызывается запуск анимации круга.

На устройстве при вызове draw метода view он вызовет applyLegacyAnimation метод, который, в свою очередь, вызовет AnimationStart метод. При этом будет вызван AnimationEnd, метод onRefresh .

В Robolectric метод рисования View никогда не вызывается, поскольку элементы фактически не отрисовываются. Это означает, что анимация никогда не будет запущена, и, следовательно, не будет и onRefresh метода.

Я пришел к выводу, что с текущей версией Robolectric невозможно проверить, что onRefresh вызвано из-за ограничений реализации. Похоже, что в будущем планируется реалистичный рендеринг.

Комментарии:

1. Спасибо за подробное объяснение. Я согласен с вами, я провел почти такое же расследование. В Espresso на реальном устройстве тест проходит.

Ответ №2:

Я, наконец, смог решить это, используя хакерский способ :

 fun swipeToRefresh(): ViewAction {
    return object : ViewAction {
        override fun getConstraints(): Matcher<View>? {
            return object : BaseMatcher<View>() {
                override fun matches(item: Any): Boolean {
                    return isA(SwipeRefreshLayout::class.java).matches(item)
                }
                override fun describeMismatch(item: Any, mismatchDescription: Description) {
                    mismatchDescription.appendText(
                        "Expected SwipeRefreshLayout or its Descendant, but got other View"
                    )
                }
                override fun describeTo(description: Description) {
                    description.appendText(
                        "Action SwipeToRefresh to view SwipeRefreshLayout or its descendant"
                    )
                }
            }
        }

        override fun getDescription(): String {
            return "Perform swipeToRefresh on the SwipeRefreshLayout"
        }

        override fun perform(uiController: UiController, view: View) {
            val swipeRefreshLayout = view as SwipeRefreshLayout
            swipeRefreshLayout.run {
                isRefreshing = true
                // set mNotify to true
                val notify = SwipeRefreshLayout::class.memberProperties.find {
                    it.name == "mNotify"
                }
                notify?.isAccessible = true
                if (notify is KMutableProperty<*>) {
                    notify.setter.call(this, true)
                }
                // mockk mRefreshListener onAnimationEnd
                val refreshListener = SwipeRefreshLayout::class.memberProperties.find {
                    it.name == "mRefreshListener"
                }
                refreshListener?.isAccessible = true
                val animatorListener = refreshListener?.get(this) as Animation.AnimationListener
                animatorListener.onAnimationEnd(mockk())
            }
        }
    }
}