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
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import org.fnives.test.showcase.BuildConfig
import org.fnives.test.showcase.TestShowcaseApplication
import org.fnives.test.showcase.di.createAppModules
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.storage.LocalDatabase
import org.junit.rules.TestRule
import org.junit.runner.Description
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.mp.KoinPlatformTools
import org.koin.test.KoinTest
import org.koin.test.get
/**
* 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.
*/
class ReloadKoinModulesIfNecessaryTestRule : TestRule, KoinTest {
class ReloadKoinModulesIfNecessaryTestRule : TestRule {
override fun apply(base: Statement, description: Description): Statement =
ReinitKoinStatement(base)
class ReinitKoinStatement(private val base: Statement) : Statement() {
class ReinitKoinStatement(private val base: Statement) : Statement(), KoinTest {
override fun evaluate() {
reinitKoinIfNeeded()
try {
base.evaluate()
} finally {
closeDB()
stopKoin()
}
}
@ -48,5 +52,13 @@ class ReloadKoinModulesIfNecessaryTestRule : TestRule, KoinTest {
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.android.testutil.intent.notIntended
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.model.content.Content
import org.fnives.test.showcase.model.content.FavouriteContent
@ -29,6 +30,16 @@ import org.hamcrest.Matchers.allOf
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() {
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
@ -50,6 +61,7 @@ class HomeRobot {
}
fun assertContainsItem(index: Int, item: FavouriteContent) = apply {
removeItemAnimations()
val isFavouriteResourceId = if (item.isFavourite) {
R.drawable.favorite_24
} else {
@ -69,6 +81,7 @@ class HomeRobot {
}
fun clickOnContentItem(index: Int, item: Content) = apply {
removeItemAnimations()
Espresso.onView(withId(R.id.recycler))
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(index))
@ -91,6 +104,7 @@ class HomeRobot {
}
fun assertContainsNoItems() = apply {
removeItemAnimations()
Espresso.onView(withId(R.id.recycler))
.check(matches(hasChildCount(0)))
}

View file

@ -6,11 +6,18 @@ import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadF
import java.util.concurrent.Executors
// 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() {
if (!anyResourceNotIdle()) return
val idlingRegistry = IdlingRegistry.getInstance()
if (idlingRegistry.resources.all(IdlingResource::isIdleNow)) return
val executor = Executors.newSingleThreadExecutor()
var isIdle = false
@ -22,6 +29,7 @@ fun awaitIdlingResources() {
idlingResource.awaitUntilIdle()
}
} while (!idlingRegistry.resources.all(IdlingResource::isIdleNow))
OkHttp3IdlingResource.sleepForDispatcherDefaultCallInRetrofitErrorState()
isIdle = true
}
while (!isIdle) {

View file

@ -21,6 +21,7 @@ class OkHttp3IdlingResource private constructor(
init {
val currentCallback = dispatcher.idleCallback
dispatcher.idleCallback = Runnable {
sleepForDispatcherDefaultCallInRetrofitErrorState()
callback?.onTransitionToIdle()
currentCallback?.run()
}
@ -46,5 +47,20 @@ class OkHttp3IdlingResource private constructor(
if (client == null) throw NullPointerException("client == null")
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) {
if (Looper.getMainLooper().thread == Thread.currentThread()) {
Thread.sleep(200L)
Thread.sleep(delay)
} else {
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()
}
}