Issue#41 Attempt to fix failing tests with compose + update test paths for artifact
This commit is contained in:
parent
60cfb46ccf
commit
f03c9f7bf2
16 changed files with 227 additions and 159 deletions
|
|
@ -1,31 +1,31 @@
|
|||
package org.fnives.test.showcase.hilt.ui.compose
|
||||
|
||||
import androidx.compose.ui.test.MainTestClock
|
||||
import androidx.compose.ui.test.junit4.StateRestorationTester
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule
|
||||
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
|
||||
import org.fnives.test.showcase.android.testutil.viewaction.LoopMainThreadFor
|
||||
import org.fnives.test.showcase.hilt.R
|
||||
import org.fnives.test.showcase.hilt.compose.screen.AppNavigation
|
||||
import org.fnives.test.showcase.hilt.core.integration.fake.FakeUserDataLocalStorage
|
||||
import org.fnives.test.showcase.hilt.di.TestUserDataLocalStorageModule
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.MockServerScenarioSetupTestRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.testutils.idling.DatabaseDispatcherTestRule
|
||||
import org.fnives.test.showcase.hilt.test.shared.ui.NetworkSynchronizedActivityTest
|
||||
import org.fnives.test.showcase.hilt.ui.compose.idle.ComposeNetworkSyncHelper
|
||||
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.runner.RunWith
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
|
||||
class AuthComposeInstrumentedTest {
|
||||
|
||||
private val composeTestRule = createComposeRule()
|
||||
private val stateRestorationTester = StateRestorationTester(composeTestRule)
|
||||
|
|
@ -36,6 +36,12 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
|
|||
private lateinit var robot: ComposeLoginRobot
|
||||
private lateinit var navigationRobot: ComposeNavigationRobot
|
||||
|
||||
@Inject
|
||||
lateinit var composeNetworkSyncHelper: ComposeNetworkSyncHelper
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
|
||||
|
|
@ -44,16 +50,22 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
|
|||
.around(composeTestRule)
|
||||
.around(ScreenshotRule("test-showcase-compose"))
|
||||
|
||||
override fun setupBeforeInjection() {
|
||||
@Before
|
||||
fun setup() {
|
||||
TestUserDataLocalStorageModule.replacement = FakeUserDataLocalStorage()
|
||||
}
|
||||
hiltRule.inject()
|
||||
|
||||
override fun setupAfterInjection() {
|
||||
stateRestorationTester.setContent {
|
||||
AppNavigation()
|
||||
}
|
||||
robot = ComposeLoginRobot(composeTestRule)
|
||||
navigationRobot = ComposeNavigationRobot(composeTestRule)
|
||||
composeNetworkSyncHelper.setup(composeTestRule)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
composeNetworkSyncHelper.tearDown()
|
||||
}
|
||||
|
||||
/** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */
|
||||
|
|
@ -76,11 +88,10 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
|
|||
robot.assertLoading()
|
||||
composeTestRule.mainClock.autoAdvance = true
|
||||
|
||||
composeTestRule.mainClock.awaitIdlingResources()
|
||||
composeTestRule.waitForIdle()
|
||||
navigationRobot.assertHomeScreen()
|
||||
}
|
||||
|
||||
/** GIVEN empty password and username WHEN signIn THEN error password is shown */
|
||||
@Test
|
||||
fun emptyPasswordShowsProperErrorMessage() {
|
||||
composeTestRule.mainClock.advanceTimeBy(SPLASH_DELAY)
|
||||
|
|
@ -90,7 +101,7 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
|
|||
.assertUsername("banan")
|
||||
.clickOnLogin()
|
||||
|
||||
composeTestRule.mainClock.awaitIdlingResources()
|
||||
composeTestRule.waitForIdle()
|
||||
robot.assertErrorIsShown(R.string.password_is_invalid)
|
||||
.assertNotLoading()
|
||||
navigationRobot.assertAuthScreen()
|
||||
|
|
@ -107,7 +118,7 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
|
|||
.assertPassword("banan")
|
||||
.clickOnLogin()
|
||||
|
||||
composeTestRule.mainClock.awaitIdlingResources()
|
||||
composeTestRule.waitForIdle()
|
||||
robot.assertErrorIsShown(R.string.username_is_invalid)
|
||||
.assertNotLoading()
|
||||
navigationRobot.assertAuthScreen()
|
||||
|
|
@ -133,7 +144,7 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
|
|||
robot.assertLoading()
|
||||
composeTestRule.mainClock.autoAdvance = true
|
||||
|
||||
composeTestRule.mainClock.awaitIdlingResources()
|
||||
composeTestRule.waitForIdle()
|
||||
robot.assertErrorIsShown(R.string.credentials_invalid)
|
||||
.assertNotLoading()
|
||||
navigationRobot.assertAuthScreen()
|
||||
|
|
@ -159,7 +170,7 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
|
|||
robot.assertLoading()
|
||||
composeTestRule.mainClock.autoAdvance = true
|
||||
|
||||
composeTestRule.mainClock.awaitIdlingResources()
|
||||
composeTestRule.waitForIdle()
|
||||
robot.assertErrorIsShown(R.string.something_went_wrong)
|
||||
.assertNotLoading()
|
||||
navigationRobot.assertAuthScreen()
|
||||
|
|
@ -185,15 +196,5 @@ class AuthComposeInstrumentedTest : NetworkSynchronizedActivityTest() {
|
|||
|
||||
companion object {
|
||||
private const val SPLASH_DELAY = 600L
|
||||
|
||||
// workaround, issue with idlingResources is tracked here https://github.com/robolectric/robolectric/issues/4807
|
||||
/**
|
||||
* Await the idling resource on a different thread while looping main.
|
||||
*/
|
||||
fun MainTestClock.awaitIdlingResources() {
|
||||
Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainThreadFor(100L))
|
||||
|
||||
advanceTimeByFrame()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package org.fnives.test.showcase.hilt.ui.compose
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertTextContains
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import androidx.compose.ui.test.onAllNodesWithTag
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
|
|
@ -13,8 +13,8 @@ import androidx.test.core.app.ApplicationProvider
|
|||
import org.fnives.test.showcase.hilt.compose.screen.auth.AuthScreenTag
|
||||
|
||||
class ComposeLoginRobot(
|
||||
composeTestRule: ComposeTestRule,
|
||||
) : ComposeTestRule by composeTestRule {
|
||||
semanticsNodeInteractionsProvider: SemanticsNodeInteractionsProvider,
|
||||
) : SemanticsNodeInteractionsProvider by semanticsNodeInteractionsProvider {
|
||||
|
||||
fun setUsername(username: String): ComposeLoginRobot = apply {
|
||||
onNodeWithTag(AuthScreenTag.UsernameInput).performTextInput(username)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package org.fnives.test.showcase.hilt.ui.compose.idle
|
||||
|
||||
import androidx.compose.ui.test.IdlingResource
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.Disposable
|
||||
|
||||
class ComposeIdlingDisposable(
|
||||
private val idlingResource: IdlingResource,
|
||||
private val testRule: ComposeTestRule,
|
||||
) : Disposable {
|
||||
override var isDisposed: Boolean = false
|
||||
private set
|
||||
|
||||
init {
|
||||
testRule.registerIdlingResource(idlingResource)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
isDisposed = true
|
||||
testRule.unregisterIdlingResource(idlingResource)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package org.fnives.test.showcase.hilt.ui.compose.idle
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.CompositeDisposable
|
||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.Disposable
|
||||
import org.fnives.test.showcase.hilt.network.testutil.NetworkSynchronization
|
||||
import javax.inject.Inject
|
||||
|
||||
class ComposeNetworkSyncHelper @Inject constructor(
|
||||
private val networkSynchronization: NetworkSynchronization,
|
||||
) {
|
||||
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
fun setup(composeTestRule: ComposeTestRule) {
|
||||
disposable = networkSynchronization.networkIdlingResources()
|
||||
.map(::EspressoToComposeIdlingResourceAdapter)
|
||||
.map { ComposeIdlingDisposable(it, composeTestRule) }
|
||||
.let(::CompositeDisposable)
|
||||
}
|
||||
|
||||
fun tearDown() {
|
||||
if (disposable == null) {
|
||||
Log.w("ComposeNetworkSyncHelper", "tearDown called, but setup wasn't!")
|
||||
}
|
||||
disposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.fnives.test.showcase.hilt.ui.compose.idle
|
||||
|
||||
import androidx.test.espresso.IdlingResource
|
||||
|
||||
class EspressoToComposeIdlingResourceAdapter(private val idlingResource: IdlingResource) : androidx.compose.ui.test.IdlingResource {
|
||||
override val isIdleNow: Boolean get() = idlingResource.isIdleNow
|
||||
}
|
||||
|
|
@ -40,4 +40,5 @@ dependencies {
|
|||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation project(':mockserver')
|
||||
implementation "androidx.test.espresso:espresso-core:$espresso_version"
|
||||
implementation project(":test-util-android")
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package org.fnives.test.showcase.hilt.network.testutil
|
|||
import androidx.annotation.CheckResult
|
||||
import androidx.test.espresso.IdlingResource
|
||||
import okhttp3.OkHttpClient
|
||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.OkHttp3IdlingResource
|
||||
import org.fnives.test.showcase.hilt.network.di.SessionLessQualifier
|
||||
import org.fnives.test.showcase.hilt.network.di.SessionQualifier
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
package org.fnives.test.showcase.hilt.network.testutil
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.test.espresso.IdlingResource
|
||||
import okhttp3.Dispatcher
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
/**
|
||||
* AndroidX version of Jake Wharton's OkHttp3IdlingResource.
|
||||
*
|
||||
* Reference: https://github.com/JakeWharton/okhttp-idling-resource/blob/master/src/main/java/com/jakewharton/espresso/OkHttp3IdlingResource.java
|
||||
*/
|
||||
class OkHttp3IdlingResource private constructor(
|
||||
private val name: String,
|
||||
private val dispatcher: Dispatcher
|
||||
) : IdlingResource {
|
||||
@Volatile
|
||||
var callback: IdlingResource.ResourceCallback? = null
|
||||
private var isIdleCallbackWasCalled: Boolean = true
|
||||
|
||||
init {
|
||||
val currentCallback = dispatcher.idleCallback
|
||||
dispatcher.idleCallback = Runnable {
|
||||
sleepForDispatcherDefaultCallInRetrofitErrorState()
|
||||
callback?.onTransitionToIdle()
|
||||
currentCallback?.run()
|
||||
isIdleCallbackWasCalled = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getName(): String = name
|
||||
|
||||
override fun isIdleNow(): Boolean {
|
||||
val isIdle = dispatcher.runningCallsCount() == 0
|
||||
if (isIdle) {
|
||||
// sometime the callback is just not properly called it seems, or maybe sync error.
|
||||
// if it isn't called Espresso crashes, so we add this here.
|
||||
callback?.onTransitionToIdle()
|
||||
}
|
||||
return isIdle
|
||||
}
|
||||
|
||||
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
|
||||
this.callback = callback
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a new [IdlingResource] from `client` as `name`. You must register
|
||||
* this instance using `Espresso.registerIdlingResources`.
|
||||
*/
|
||||
@CheckResult
|
||||
@NonNull
|
||||
fun create(@NonNull name: String?, @NonNull client: OkHttpClient?): OkHttp3IdlingResource {
|
||||
if (name == null) throw NullPointerException("name == null")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue