Issue#13 Remove unnecessary configurations and use TestDispatcher on both side

Additionally added workaround for progressbar testing
This commit is contained in:
Gergely Hegedus 2022-01-27 01:54:43 +02:00
parent b9644512d5
commit 5d89e62356
19 changed files with 108 additions and 174 deletions

View file

@ -1,5 +0,0 @@
package org.fnives.test.showcase.testutils.configuration
object AndroidTestLoginRobotConfiguration : LoginRobotConfiguration {
override val assertLoadingBeforeRequest: Boolean get() = false
}

View file

@ -1,51 +0,0 @@
package org.fnives.test.showcase.testutils.configuration
import androidx.test.espresso.Espresso
import androidx.test.espresso.NoActivityResumedException
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import kotlinx.coroutines.Dispatchers
import org.fnives.test.showcase.storage.database.DatabaseInitialization
import org.fnives.test.showcase.testutils.idling.loopMainThreadFor
import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources
import org.junit.runner.Description
import org.junit.runners.model.Statement
class AndroidTestMainDispatcherTestRule : MainDispatcherTestRule {
override fun apply(base: Statement, description: Description): Statement =
object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
DatabaseInitialization.dispatcher = Dispatchers.Main
base.evaluate()
}
}
override fun advanceUntilIdleWithIdlingResources() {
loopMainThreadUntilIdleWithIdlingResources()
}
override fun advanceUntilIdleOrActivityIsDestroyed() {
try {
advanceUntilIdleWithIdlingResources()
Espresso.onView(ViewMatchers.isRoot()).check(ViewAssertions.doesNotExist())
} catch (noActivityResumedException: NoActivityResumedException) {
// expected to happen
} catch (runtimeException: RuntimeException) {
if (runtimeException.message?.contains("No activities found") == true) {
// expected to happen
} else {
throw runtimeException
}
}
}
override fun advanceUntilIdle() {
loopMainThreadUntilIdleWithIdlingResources()
}
override fun advanceTimeBy(delayInMillis: Long) {
loopMainThreadFor(delayInMillis)
}
}

View file

@ -1,11 +1,6 @@
package org.fnives.test.showcase.testutils.configuration
object SpecificTestConfigurationsFactory : TestConfigurationsFactory {
override fun createMainDispatcherTestRule(): MainDispatcherTestRule =
AndroidTestMainDispatcherTestRule()
override fun createLoginRobotConfiguration(): LoginRobotConfiguration =
AndroidTestLoginRobotConfiguration
override fun createSnackbarVerification(): SnackbarVerificationHelper =
AndroidTestSnackbarVerificationHelper

View file

@ -1,5 +0,0 @@
package org.fnives.test.showcase.testutils.configuration
object RobolectricLoginRobotConfiguration : LoginRobotConfiguration {
override val assertLoadingBeforeRequest: Boolean = true
}

View file

@ -1,11 +1,6 @@
package org.fnives.test.showcase.testutils.configuration
object SpecificTestConfigurationsFactory : TestConfigurationsFactory {
override fun createMainDispatcherTestRule(): MainDispatcherTestRule =
TestCoroutineMainDispatcherTestRule()
override fun createLoginRobotConfiguration(): LoginRobotConfiguration =
RobolectricLoginRobotConfiguration
override fun createSnackbarVerification(): SnackbarVerificationHelper =
RobolectricSnackbarVerificationHelper

View file

@ -0,0 +1,22 @@
package org.fnives.test.showcase.testutils
import android.app.Activity
import androidx.test.core.app.ActivityScenario
fun <T: Activity> ActivityScenario<T>.safeClose() {
workaroundForActivityScenarioCLoseLockingUp()
close()
}
/**
* This should not be needed, we shouldn't use sleep ever.
* However, it seems to be and issue described here: https://github.com/android/android-test/issues/676
*
* If an activity is finished in code, the ActivityScenario.close() can hang 30 to 45 seconds.
* This sleeps let's the Activity finish it state change and unlocks the ActivityScenario.
*
* As soon as that issue is closed, this should be removed as well.
*/
private fun workaroundForActivityScenarioCLoseLockingUp() {
Thread.sleep(1000L)
}

View file

@ -1,6 +0,0 @@
package org.fnives.test.showcase.testutils.configuration
interface LoginRobotConfiguration {
val assertLoadingBeforeRequest: Boolean
}

View file

@ -1,18 +0,0 @@
package org.fnives.test.showcase.testutils.configuration
import org.junit.rules.TestRule
interface MainDispatcherTestRule : TestRule {
fun advanceUntilIdleWithIdlingResources()
fun advanceUntilIdleOrActivityIsDestroyed()
fun advanceUntilIdle()
fun advanceTimeBy(delayInMillis: Long)
}
@Suppress("TestFunctionName")
fun MainDispatcherTestRule(): MainDispatcherTestRule =
SpecificTestConfigurationsFactory.createMainDispatcherTestRule()

View file

@ -8,10 +8,6 @@ package org.fnives.test.showcase.testutils.configuration
*/
interface TestConfigurationsFactory {
fun createMainDispatcherTestRule(): MainDispatcherTestRule
fun createLoginRobotConfiguration(): LoginRobotConfiguration
fun createSnackbarVerification(): SnackbarVerificationHelper
fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory

View file

@ -5,7 +5,7 @@ import android.os.Looper
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
fun doBlockinglyOnMainThread(action: () -> Unit) {
fun runOnUIAwaitOnCurrent(action: () -> Unit) {
if (Looper.myLooper() === Looper.getMainLooper()) {
action()
} else {

View file

@ -1,4 +1,4 @@
package org.fnives.test.showcase.testutils.configuration
package org.fnives.test.showcase.testutils.idling
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -8,12 +8,13 @@ import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.fnives.test.showcase.storage.database.DatabaseInitialization
import org.fnives.test.showcase.testutils.idling.advanceUntilIdleWithIdlingResources
import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@OptIn(ExperimentalCoroutinesApi::class)
class TestCoroutineMainDispatcherTestRule : MainDispatcherTestRule {
class MainDispatcherTestRule : TestRule {
private lateinit var testDispatcher: TestDispatcher
@ -33,19 +34,24 @@ class TestCoroutineMainDispatcherTestRule : MainDispatcherTestRule {
}
}
override fun advanceUntilIdleWithIdlingResources() {
fun advanceUntilIdleWithIdlingResources() = runOnUIAwaitOnCurrent {
testDispatcher.advanceUntilIdleWithIdlingResources()
}
override fun advanceUntilIdleOrActivityIsDestroyed() {
advanceUntilIdleWithIdlingResources()
}
override fun advanceUntilIdle() {
fun advanceUntilIdle() = runOnUIAwaitOnCurrent {
testDispatcher.scheduler.advanceUntilIdle()
}
override fun advanceTimeBy(delayInMillis: Long) {
fun advanceTimeBy(delayInMillis: Long) = runOnUIAwaitOnCurrent {
testDispatcher.scheduler.advanceTimeBy(delayInMillis)
}
private fun TestDispatcher.advanceUntilIdleWithIdlingResources() {
scheduler.advanceUntilIdle() // advance until a request is sent
while (anyResourceIdling()) { // check if any request is in progress
awaitIdlingResources() // complete all requests and other idling resources
scheduler.advanceUntilIdle() // run coroutines after request is finished
}
scheduler.advanceUntilIdle()
}
}

View file

@ -1,11 +1,10 @@
package org.fnives.test.showcase.testutils.idling
import android.os.Looper
import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.matcher.ViewMatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import org.fnives.test.showcase.testutils.viewactions.LoopMainThreadFor
import org.fnives.test.showcase.testutils.viewactions.LoopMainThreadUntilIdle
import java.util.concurrent.Executors
@ -43,16 +42,6 @@ private fun IdlingResource.awaitUntilIdle() {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun TestDispatcher.advanceUntilIdleWithIdlingResources() {
scheduler.advanceUntilIdle() // advance until a request is sent
while (anyResourceIdling()) { // check if any request is in progress
awaitIdlingResources() // complete all requests and other idling resources
scheduler.advanceUntilIdle() // run coroutines after request is finished
}
scheduler.advanceUntilIdle()
}
fun loopMainThreadUntilIdleWithIdlingResources() {
Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadUntilIdle()) // advance until a request is sent
while (anyResourceIdling()) { // check if any request is in progress
@ -63,5 +52,9 @@ fun loopMainThreadUntilIdleWithIdlingResources() {
}
fun loopMainThreadFor(delay: Long) {
if (Looper.getMainLooper().isCurrentThread){
Thread.sleep(200L)
} else {
Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(delay))
}
}

View file

@ -6,7 +6,8 @@ import androidx.test.espresso.intent.Intents
import androidx.test.runner.intent.IntentStubberRegistry
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.safeClose
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.home.HomeRobot
import org.fnives.test.showcase.ui.home.MainActivity
@ -30,9 +31,9 @@ object SetupAuthenticationState : KoinTest {
.setUsername("a")
.clickOnLogin()
mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
activityScenario.close()
activityScenario.safeClose()
resetIntentsIfNeeded(resetIntents)
}
@ -43,10 +44,9 @@ object SetupAuthenticationState : KoinTest {
val activityScenario = ActivityScenario.launch(MainActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
HomeRobot().clickSignOut()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed()
activityScenario.close()
activityScenario.safeClose()
resetIntentsIfNeeded(resetIntents)
}

View file

@ -5,7 +5,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.listener
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import org.fnives.test.showcase.testutils.doBlockinglyOnMainThread
import org.fnives.test.showcase.testutils.runOnUIAwaitOnCurrent
import org.hamcrest.BaseMatcher
import org.hamcrest.CoreMatchers.isA
import org.hamcrest.Description
@ -36,7 +36,7 @@ class PullToRefresh : ViewAction {
override fun perform(uiController: UiController, view: View) {
val swipeRefreshLayout = view as SwipeRefreshLayout
doBlockinglyOnMainThread {
runOnUIAwaitOnCurrent {
swipeRefreshLayout.isRefreshing = true
swipeRefreshLayout.listener().onRefresh()
}

View file

@ -0,0 +1,24 @@
package org.fnives.test.showcase.testutils.viewactions
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.View
import android.widget.ProgressBar
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import org.hamcrest.Matcher
class ReplaceProgressBarDrawableToStatic : ViewAction {
override fun getConstraints(): Matcher<View> =
isAssignableFrom(ProgressBar::class.java)
override fun getDescription(): String =
"replace the ProgressBar drawable"
override fun perform(uiController: UiController, view: View) {
val progressBar: ProgressBar = view as ProgressBar
progressBar.indeterminateDrawable = ColorDrawable(Color.GREEN)
uiController.loopMainThreadUntilIdle()
}
}

View file

@ -7,10 +7,11 @@ import org.fnives.test.showcase.network.mockserver.ContentData
import org.fnives.test.showcase.network.mockserver.scenario.content.ContentScenario
import org.fnives.test.showcase.network.mockserver.scenario.refresh.RefreshTokenScenario
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.idling.loopMainThreadFor
import org.fnives.test.showcase.testutils.idling.loopMainThreadUntilIdleWithIdlingResources
import org.fnives.test.showcase.testutils.robot.RobotTestRule
import org.fnives.test.showcase.testutils.safeClose
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin
import org.junit.After
import org.junit.Before
@ -45,7 +46,7 @@ class MainActivityTest : KoinTest {
@After
fun tearDown() {
activityScenario.close()
activityScenario.safeClose()
}
/** GIVEN initialized MainActivity WHEN signout is clicked THEN user is signed out */
@ -56,7 +57,7 @@ class MainActivityTest : KoinTest {
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.clickSignOut()
mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertNavigatedToAuth()
}
@ -101,7 +102,7 @@ class MainActivityTest : KoinTest {
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true)
activityScenario.close()
activityScenario.safeClose()
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()

View file

@ -5,8 +5,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fnives.test.showcase.R
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.robot.RobotTestRule
import org.fnives.test.showcase.testutils.safeClose
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.junit.After
import org.junit.Rule
@ -34,7 +35,7 @@ class AuthActivityTest : KoinTest {
@After
fun tearDown() {
activityScenario.close()
activityScenario.safeClose()
}
/** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */
@ -52,7 +53,7 @@ class AuthActivityTest : KoinTest {
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleOrActivityIsDestroyed()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertNavigatedToHome()
}

View file

@ -15,27 +15,18 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.fnives.test.showcase.R
import org.fnives.test.showcase.testutils.configuration.LoginRobotConfiguration
import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationHelper
import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationTestRule
import org.fnives.test.showcase.testutils.configuration.SpecificTestConfigurationsFactory
import org.fnives.test.showcase.testutils.configuration.TestConfigurationsFactory
import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.ReplaceProgressBarDrawableToStatic
import org.fnives.test.showcase.testutils.viewactions.notIntended
import org.fnives.test.showcase.ui.home.MainActivity
import org.hamcrest.core.IsNot.not
class LoginRobot(
private val loginRobotConfiguration: LoginRobotConfiguration,
private val snackbarVerificationHelper: SnackbarVerificationHelper
private val snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationTestRule()
) : Robot {
constructor(testConfigurationsFactory: TestConfigurationsFactory = SpecificTestConfigurationsFactory) :
this(
loginRobotConfiguration = testConfigurationsFactory.createLoginRobotConfiguration(),
snackbarVerificationHelper = SnackbarVerificationTestRule()
)
override fun init() {
Intents.init()
setupIntentResults()
@ -50,6 +41,18 @@ class LoginRobot(
Intents.release()
}
/**
* Needed because Espresso idling waits until mainThread is idle.
*
* However, ProgressBar keeps the main thread active since it's animating.
*
* Another solution is described here: https://proandroiddev.com/progressbar-animations-with-espresso-57f826102187
* In short they replace the inflater to remove animations, by using custom test runner.
*/
fun replaceProgressBar() = apply {
onView(withId(R.id.loading_indicator)).perform(ReplaceProgressBarDrawableToStatic())
}
fun setUsername(username: String): LoginRobot = apply {
onView(withId(R.id.user_edit_text))
.perform(ViewActions.replaceText(username), ViewActions.closeSoftKeyboard())
@ -61,6 +64,7 @@ class LoginRobot(
}
fun clickOnLogin() = apply {
replaceProgressBar()
onView(withId(R.id.login_cta))
.perform(ViewActions.click())
}
@ -80,11 +84,9 @@ class LoginRobot(
}
fun assertLoadingBeforeRequests() = apply {
if (loginRobotConfiguration.assertLoadingBeforeRequest) {
onView(withId(R.id.loading_indicator))
.check(ViewAssertions.matches(isDisplayed()))
}
}
fun assertNotLoading() = apply {
onView(withId(R.id.loading_indicator))

View file

@ -4,8 +4,9 @@ import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.configuration.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.robot.RobotTestRule
import org.fnives.test.showcase.testutils.safeClose
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogout
import org.junit.After
@ -34,7 +35,7 @@ class SplashActivityTest : KoinTest {
@After
fun tearDown() {
activityScenario.close()
activityScenario.safeClose()
}
/** GIVEN loggedInState WHEN opened after some time THEN MainActivity is started */
@ -49,8 +50,6 @@ class SplashActivityTest : KoinTest {
robot.assertHomeIsStarted()
.assertAuthIsNotStarted()
workaroundForActivityScenarioCLoseLockingUp()
}
/** GIVEN loggedOffState WHEN opened after some time THEN AuthActivity is started */
@ -64,8 +63,6 @@ class SplashActivityTest : KoinTest {
robot.assertAuthIsStarted()
.assertHomeIsNotStarted()
workaroundForActivityScenarioCLoseLockingUp()
}
/** GIVEN loggedOffState and not enough time WHEN opened THEN no activity is started */
@ -75,7 +72,7 @@ class SplashActivityTest : KoinTest {
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
mainDispatcherTestRule.advanceTimeBy(10)
mainDispatcherTestRule.advanceTimeBy(500)
robot.assertAuthIsNotStarted()
.assertHomeIsNotStarted()
@ -89,22 +86,9 @@ class SplashActivityTest : KoinTest {
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
mainDispatcherTestRule.advanceTimeBy(10)
mainDispatcherTestRule.advanceTimeBy(500)
robot.assertHomeIsNotStarted()
.assertAuthIsNotStarted()
}
/**
* This should not be needed, we shouldn't use sleep ever.
* However, it seems to be and issue described here: https://github.com/android/android-test/issues/676
*
* If an activity is finished in code, the ActivityScenario.close() can hang 30 to 45 seconds.
* This sleeps let's the Activity finish it state change and unlocks the ActivityScenario.
*
* As soon as that issue is closed, this should be removed as well.
*/
private fun workaroundForActivityScenarioCLoseLockingUp() {
Thread.sleep(1000L)
}
}