#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())
}
}
}
}