Merge pull request #115 from fknives/issue#103-try-solutions

Issue#103 try solutions
This commit is contained in:
Gergely Hegedis 2022-07-20 20:45:57 +03:00 committed by GitHub
commit 2ddc933dc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 80 additions and 5 deletions

View file

@ -1,10 +1,12 @@
package org.fnives.test.showcase.testutils package org.fnives.test.showcase.testutils
import android.util.Log
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import org.fnives.test.showcase.BuildConfig import org.fnives.test.showcase.BuildConfig
import org.fnives.test.showcase.TestShowcaseApplication import org.fnives.test.showcase.TestShowcaseApplication
import org.fnives.test.showcase.di.createAppModules import org.fnives.test.showcase.di.createAppModules
import org.fnives.test.showcase.model.network.BaseUrl import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.storage.LocalDatabase
import org.junit.rules.TestRule import org.junit.rules.TestRule
import org.junit.runner.Description import org.junit.runner.Description
import org.junit.runners.model.Statement import org.junit.runners.model.Statement
@ -14,6 +16,7 @@ import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin import org.koin.core.context.stopKoin
import org.koin.mp.KoinPlatformTools import org.koin.mp.KoinPlatformTools
import org.koin.test.KoinTest import org.koin.test.KoinTest
import org.koin.test.get
/** /**
* Test rule to help reinitialize the whole Koin setup. * Test rule to help reinitialize the whole Koin setup.
@ -23,16 +26,17 @@ import org.koin.test.KoinTest
* *
* Note: Do not use if you want your test's to share Koin, and in such case do not stop your Koin. * Note: Do not use if you want your test's to share Koin, and in such case do not stop your Koin.
*/ */
class ReloadKoinModulesIfNecessaryTestRule : TestRule, KoinTest { class ReloadKoinModulesIfNecessaryTestRule : TestRule {
override fun apply(base: Statement, description: Description): Statement = override fun apply(base: Statement, description: Description): Statement =
ReinitKoinStatement(base) ReinitKoinStatement(base)
class ReinitKoinStatement(private val base: Statement) : Statement() { class ReinitKoinStatement(private val base: Statement) : Statement(), KoinTest {
override fun evaluate() { override fun evaluate() {
reinitKoinIfNeeded() reinitKoinIfNeeded()
try { try {
base.evaluate() base.evaluate()
} finally { } finally {
closeDB()
stopKoin() stopKoin()
} }
} }
@ -48,5 +52,13 @@ class ReloadKoinModulesIfNecessaryTestRule : TestRule, KoinTest {
modules(createAppModules(baseUrl)) modules(createAppModules(baseUrl))
} }
} }
private fun closeDB() {
try {
get<LocalDatabase>().close()
} catch (ignored: Throwable) {
Log.d("ReloadKoinModulesRule", "Could not close db: $ignored, stacktrace: ${ignored.stackTraceToString()}")
}
}
} }
} }

View file

@ -21,6 +21,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import org.fnives.test.showcase.R import org.fnives.test.showcase.R
import org.fnives.test.showcase.android.testutil.intent.notIntended import org.fnives.test.showcase.android.testutil.intent.notIntended
import org.fnives.test.showcase.android.testutil.viewaction.imageview.WithDrawable import org.fnives.test.showcase.android.testutil.viewaction.imageview.WithDrawable
import org.fnives.test.showcase.android.testutil.viewaction.recycler.RemoveItemAnimations
import org.fnives.test.showcase.android.testutil.viewaction.swiperefresh.PullToRefresh import org.fnives.test.showcase.android.testutil.viewaction.swiperefresh.PullToRefresh
import org.fnives.test.showcase.model.content.Content import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.FavouriteContent import org.fnives.test.showcase.model.content.FavouriteContent
@ -29,6 +30,16 @@ import org.hamcrest.Matchers.allOf
class HomeRobot { class HomeRobot {
/**
* Needed because Espresso idling sometimes not in sync with RecyclerView's animation.
* So we simply remove the item animations, the animations should be disabled anyway for test.
*
* Reference: https://github.com/android/android-test/issues/223
*/
fun removeItemAnimations() = apply {
Espresso.onView(withId(R.id.recycler)).perform(RemoveItemAnimations())
}
fun setupIntentResults() { fun setupIntentResults() {
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName)) Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent())) .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
@ -50,6 +61,7 @@ class HomeRobot {
} }
fun assertContainsItem(index: Int, item: FavouriteContent) = apply { fun assertContainsItem(index: Int, item: FavouriteContent) = apply {
removeItemAnimations()
val isFavouriteResourceId = if (item.isFavourite) { val isFavouriteResourceId = if (item.isFavourite) {
R.drawable.favorite_24 R.drawable.favorite_24
} else { } else {
@ -69,6 +81,7 @@ class HomeRobot {
} }
fun clickOnContentItem(index: Int, item: Content) = apply { fun clickOnContentItem(index: Int, item: Content) = apply {
removeItemAnimations()
Espresso.onView(withId(R.id.recycler)) Espresso.onView(withId(R.id.recycler))
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(index)) .perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(index))
@ -91,6 +104,7 @@ class HomeRobot {
} }
fun assertContainsNoItems() = apply { fun assertContainsNoItems() = apply {
removeItemAnimations()
Espresso.onView(withId(R.id.recycler)) Espresso.onView(withId(R.id.recycler))
.check(matches(hasChildCount(0))) .check(matches(hasChildCount(0)))
} }

View file

@ -6,11 +6,18 @@ import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadF
import java.util.concurrent.Executors import java.util.concurrent.Executors
// workaround, issue with idlingResources is tracked here https://github.com/robolectric/robolectric/issues/4807 // workaround, issue with idlingResources is tracked here https://github.com/robolectric/robolectric/issues/4807
fun anyResourceNotIdle(): Boolean = (!IdlingRegistry.getInstance().resources.all(IdlingResource::isIdleNow)) fun anyResourceNotIdle(): Boolean {
val anyResourceNotIdle = (!IdlingRegistry.getInstance().resources.all(IdlingResource::isIdleNow))
if (!anyResourceNotIdle) {
// once it's idle we wait the Idling resource's time
OkHttp3IdlingResource.sleepForDispatcherDefaultCallInRetrofitErrorState()
}
return anyResourceNotIdle
}
fun awaitIdlingResources() { fun awaitIdlingResources() {
if (!anyResourceNotIdle()) return
val idlingRegistry = IdlingRegistry.getInstance() val idlingRegistry = IdlingRegistry.getInstance()
if (idlingRegistry.resources.all(IdlingResource::isIdleNow)) return
val executor = Executors.newSingleThreadExecutor() val executor = Executors.newSingleThreadExecutor()
var isIdle = false var isIdle = false
@ -22,6 +29,7 @@ fun awaitIdlingResources() {
idlingResource.awaitUntilIdle() idlingResource.awaitUntilIdle()
} }
} while (!idlingRegistry.resources.all(IdlingResource::isIdleNow)) } while (!idlingRegistry.resources.all(IdlingResource::isIdleNow))
OkHttp3IdlingResource.sleepForDispatcherDefaultCallInRetrofitErrorState()
isIdle = true isIdle = true
} }
while (!isIdle) { while (!isIdle) {

View file

@ -21,6 +21,7 @@ class OkHttp3IdlingResource private constructor(
init { init {
val currentCallback = dispatcher.idleCallback val currentCallback = dispatcher.idleCallback
dispatcher.idleCallback = Runnable { dispatcher.idleCallback = Runnable {
sleepForDispatcherDefaultCallInRetrofitErrorState()
callback?.onTransitionToIdle() callback?.onTransitionToIdle()
currentCallback?.run() currentCallback?.run()
} }
@ -46,5 +47,20 @@ class OkHttp3IdlingResource private constructor(
if (client == null) throw NullPointerException("client == null") if (client == null) throw NullPointerException("client == null")
return OkHttp3IdlingResource(name, client.dispatcher) return OkHttp3IdlingResource(name, client.dispatcher)
} }
/**
* This is required, because in case of Errors Retrofit uses Dispatcher.Default to suspendThrow
* see: retrofit2.KotlinExtensions.kt Exception.suspendAndThrow
* Relevant code issue: https://github.com/square/retrofit/blob/6cd6f7d8287f73909614cb7300fcde05f5719750/retrofit/src/main/java/retrofit2/KotlinExtensions.kt#L121
* This is the current suggested approach to their problem with Unchecked Exceptions
*
* Sadly Dispatcher.Default cannot be replaced yet, so we can't swap it out in tests:
* https://github.com/Kotlin/kotlinx.coroutines/issues/1365
*
* This brings us to this sleep for now.
*/
fun sleepForDispatcherDefaultCallInRetrofitErrorState() {
Thread.sleep(200L)
}
} }
} }

View file

@ -28,7 +28,7 @@ fun runOnUIAwaitOnCurrent(action: () -> Unit) {
fun loopMainThreadFor(delay: Long) { fun loopMainThreadFor(delay: Long) {
if (Looper.getMainLooper().thread == Thread.currentThread()) { if (Looper.getMainLooper().thread == Thread.currentThread()) {
Thread.sleep(200L) Thread.sleep(delay)
} else { } else {
Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(delay)) Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(delay))
} }

View file

@ -0,0 +1,25 @@
package org.fnives.test.showcase.android.testutil.viewaction.recycler
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers
import org.hamcrest.Matcher
/**
* Sets the [RecyclerView]'s [itemAnimator][RecyclerView.setItemAnimator] to null, thus disabling animations.
*/
class RemoveItemAnimations : ViewAction {
override fun getConstraints(): Matcher<View> =
ViewMatchers.isAssignableFrom(RecyclerView::class.java)
override fun getDescription(): String =
"Remove item animations"
override fun perform(uiController: UiController, view: View) {
val recycler: RecyclerView = view as RecyclerView
recycler.itemAnimator = null
uiController.loopMainThreadUntilIdle()
}
}