Merge pull request #115 from fknives/issue#103-try-solutions
Issue#103 try solutions
This commit is contained in:
commit
2ddc933dc2
6 changed files with 80 additions and 5 deletions
|
|
@ -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()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue