Issue#41 Attempt to fix failing tests with compose + update test paths for artifact

This commit is contained in:
Gergely Hegedus 2022-09-28 15:47:43 +03:00
parent 60cfb46ccf
commit f03c9f7bf2
16 changed files with 227 additions and 159 deletions

View file

@ -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()
}
}
}

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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

View file

@ -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)
}
}
}