Skip to content

Commit

Permalink
Merge pull request #115 from fknives/issue#103-try-solutions
Browse files Browse the repository at this point in the history
Issue#103 try solutions
  • Loading branch information
fknives authored Jul 20, 2022
2 parents 4f6bba6 + fd661ca commit 2ddc933
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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()
}
}
Expand All @@ -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()}")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()))
Expand All @@ -50,6 +61,7 @@ class HomeRobot {
}

fun assertContainsItem(index: Int, item: FavouriteContent) = apply {
removeItemAnimations()
val isFavouriteResourceId = if (item.isFavourite) {
R.drawable.favorite_24
} else {
Expand All @@ -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))

Expand All @@ -91,6 +104,7 @@ class HomeRobot {
}

fun assertContainsNoItems() = apply {
removeItemAnimations()
Espresso.onView(withId(R.id.recycler))
.check(matches(hasChildCount(0)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,6 +29,7 @@ fun awaitIdlingResources() {
idlingResource.awaitUntilIdle()
}
} while (!idlingRegistry.resources.all(IdlingResource::isIdleNow))
OkHttp3IdlingResource.sleepForDispatcherDefaultCallInRetrofitErrorState()
isIdle = true
}
while (!isIdle) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class OkHttp3IdlingResource private constructor(
init {
val currentCallback = dispatcher.idleCallback
dispatcher.idleCallback = Runnable {
sleepForDispatcherDefaultCallInRetrofitErrorState()
callback?.onTransitionToIdle()
currentCallback?.run()
}
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit 2ddc933

Please sign in to comment.