Assert navigation

This commit is contained in:
Alex Gabor 2022-04-01 14:59:36 +03:00
parent 225fbed849
commit d948d06378
11 changed files with 167 additions and 18 deletions

View file

@ -5,7 +5,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fnives.test.showcase.compose.ComposeActivity import org.fnives.test.showcase.compose.ComposeActivity
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule import org.fnives.test.showcase.testutils.idling.ComposeMainDispatcherTestRule
import org.fnives.test.showcase.testutils.idling.ComposeNetworkSynchronizationTestRule
import org.fnives.test.showcase.testutils.idling.anyResourceIdling import org.fnives.test.showcase.testutils.idling.anyResourceIdling
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -20,10 +21,11 @@ class AuthComposeInstrumentedTest : KoinTest {
@get:Rule @get:Rule
val composeTestRule = createAndroidComposeRule<ComposeActivity>() val composeTestRule = createAndroidComposeRule<ComposeActivity>()
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule() private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule(networkSynchronizationTestRule = ComposeNetworkSynchronizationTestRule(composeTestRule))
private val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup private val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
private val mainDispatcherTestRule = MainDispatcherTestRule() private val mainDispatcherTestRule = ComposeMainDispatcherTestRule()
private lateinit var robot: ComposeLoginRobot private lateinit var robot: ComposeLoginRobot
private lateinit var screenRobot: ComposeScreenRobot
@Rule @Rule
@JvmField @JvmField
@ -34,6 +36,7 @@ class AuthComposeInstrumentedTest : KoinTest {
@Before @Before
fun setup() { fun setup() {
robot = ComposeLoginRobot(composeTestRule) robot = ComposeLoginRobot(composeTestRule)
screenRobot = ComposeScreenRobot(composeTestRule)
} }
/** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */ /** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */
@ -44,19 +47,21 @@ class AuthComposeInstrumentedTest : KoinTest {
) )
composeTestRule.mainClock.advanceTimeBy(500L) composeTestRule.mainClock.advanceTimeBy(500L)
composeTestRule.mainClock.advanceTimeUntil { anyResourceIdling() } composeTestRule.mainClock.advanceTimeUntil { anyResourceIdling() }
composeTestRule.waitForIdle() screenRobot.assertAuthScreen()
robot robot
.setPassword("alma") .setPassword("alma")
.setUsername("banan") .setUsername("banan")
.assertUsername("banan") .assertUsername("banan")
.assertPassword("alma") .assertPassword("alma")
composeTestRule.mainClock.autoAdvance = false composeTestRule.mainClock.autoAdvance = false
robot.clickOnLogin() robot.clickOnLogin()
composeTestRule.mainClock.advanceTimeByFrame() composeTestRule.mainClock.advanceTimeByFrame()
robot.assertLoading() robot.assertLoading()
composeTestRule.mainClock.autoAdvance = true
// mainDispatcherTestRule.advanceUntilIdleWithIdlingResources() composeTestRule.mainClock.advanceTimeUntil { anyResourceIdling() }
// robot.assertNavigatedToHome() screenRobot.assertHomeScreen()
} }
} }

View file

@ -0,0 +1,19 @@
package org.fnives.test.showcase.ui
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithTag
import org.fnives.test.showcase.compose.screen.AppNavigationTag
class ComposeScreenRobot(
private val composeTestRule: ComposeTestRule,
) {
fun assertHomeScreen(): ComposeScreenRobot = apply {
composeTestRule.onNodeWithTag(AppNavigationTag.HomeScreen).assertIsDisplayed()
}
fun assertAuthScreen(): ComposeScreenRobot = apply {
composeTestRule.onNodeWithTag(AppNavigationTag.AuthScreen).assertIsDisplayed()
}
}

View file

@ -5,16 +5,17 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.fnives.test.showcase.compose.screen.auth.AuthScreen import org.fnives.test.showcase.compose.screen.auth.AuthScreen
import org.fnives.test.showcase.compose.screen.auth.rememberAuthScreenState import org.fnives.test.showcase.compose.screen.auth.rememberAuthScreenState
import org.fnives.test.showcase.compose.screen.home.HomeScreen import org.fnives.test.showcase.compose.screen.home.HomeScreen
import org.fnives.test.showcase.compose.screen.home.rememberHomeScreenState import org.fnives.test.showcase.compose.screen.home.rememberHomeScreenState
import org.fnives.test.showcase.compose.screen.splash.SplashScreen import org.fnives.test.showcase.compose.screen.splash.SplashScreen
import org.fnives.test.showcase.core.login.IsUserLoggedInUseCase
import org.koin.androidx.compose.get import org.koin.androidx.compose.get
@Composable @Composable
@ -35,15 +36,22 @@ fun AppNavigation() {
composable("Splash") { SplashScreen() } composable("Splash") { SplashScreen() }
composable("Auth") { composable("Auth") {
val authState = rememberAuthScreenState() val authState = rememberAuthScreenState()
AuthScreen(authState) AuthScreen(Modifier.testTag(AppNavigationTag.AuthScreen), authState)
if (authState.navigateToHome?.consume() != null) { if (authState.navigateToHome?.consume() != null) {
navController.navigate("Home") navController.navigate("Home")
} }
} }
composable("Home") { composable("Home") {
HomeScreen(rememberHomeScreenState { HomeScreen(
navController.navigate("Auth") Modifier.testTag(AppNavigationTag.HomeScreen),
}) homeScreenState = rememberHomeScreenState(
onLogout = { navController.navigate("Auth") })
)
} }
} }
}
object AppNavigationTag {
const val AuthScreen = "AppNavigationTag.AuthScreen"
const val HomeScreen = "AppNavigationTag.HomeScreen"
} }

View file

@ -26,9 +26,10 @@ import org.fnives.test.showcase.R
@Composable @Composable
fun AuthScreen( fun AuthScreen(
modifier: Modifier = Modifier,
authScreenState: AuthScreenState = rememberAuthScreenState() authScreenState: AuthScreenState = rememberAuthScreenState()
) { ) {
ConstraintLayout(Modifier.fillMaxSize()) { ConstraintLayout(modifier.fillMaxSize()) {
val (title, credentials, snackbar, loading, login) = createRefs() val (title, credentials, snackbar, loading, login) = createRefs()
Title( Title(
Modifier Modifier

View file

@ -67,7 +67,6 @@ class AuthScreenState(
LoginStatus.INVALID_USERNAME -> error = ErrorType.UNSUPPORTED_USERNAME LoginStatus.INVALID_USERNAME -> error = ErrorType.UNSUPPORTED_USERNAME
LoginStatus.INVALID_PASSWORD -> error = ErrorType.UNSUPPORTED_PASSWORD LoginStatus.INVALID_PASSWORD -> error = ErrorType.UNSUPPORTED_PASSWORD
} }
println("asdasdasd: ${error.hashCode()}")
} }
fun dismissError() { fun dismissError() {

View file

@ -26,9 +26,10 @@ import org.fnives.test.showcase.model.content.FavouriteContent
@Composable @Composable
fun HomeScreen( fun HomeScreen(
modifier: Modifier = Modifier,
homeScreenState: HomeScreenState = rememberHomeScreenState() homeScreenState: HomeScreenState = rememberHomeScreenState()
) { ) {
Column(Modifier.fillMaxSize()) { Column(modifier.fillMaxSize()) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Title(Modifier.weight(1f)) Title(Modifier.weight(1f))
Image( Image(

View file

@ -17,7 +17,7 @@ import org.koin.test.KoinTest
*/ */
class MockServerScenarioSetupResetingTestRule( class MockServerScenarioSetupResetingTestRule(
private val reloadKoinModulesIfNecessaryTestRule: ReloadKoinModulesIfNecessaryTestRule = ReloadKoinModulesIfNecessaryTestRule(), private val reloadKoinModulesIfNecessaryTestRule: ReloadKoinModulesIfNecessaryTestRule = ReloadKoinModulesIfNecessaryTestRule(),
private val networkSynchronizationTestRule: NetworkSynchronizationTestRule = NetworkSynchronizationTestRule() private val networkSynchronizationTestRule: TestRule = NetworkSynchronizationTestRule()
) : TestRule, KoinTest { ) : TestRule, KoinTest {
lateinit var mockServerScenarioSetup: MockServerScenarioSetup lateinit var mockServerScenarioSetup: MockServerScenarioSetup

View file

@ -0,0 +1,50 @@
package org.fnives.test.showcase.testutils.idling
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import org.fnives.test.showcase.storage.database.DatabaseInitialization
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 ComposeMainDispatcherTestRule : TestRule {
private lateinit var testDispatcher: TestDispatcher
override fun apply(base: Statement, description: Description): Statement =
object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
val dispatcher = StandardTestDispatcher()
testDispatcher = dispatcher
DatabaseInitialization.dispatcher = dispatcher
base.evaluate()
}
}
fun advanceUntilIdleWithIdlingResources() = runOnUIAwaitOnCurrent {
testDispatcher.advanceUntilIdleWithIdlingResources()
}
fun advanceUntilIdle() = runOnUIAwaitOnCurrent {
testDispatcher.scheduler.advanceUntilIdle()
}
fun advanceTimeBy(delayInMillis: Long) = runOnUIAwaitOnCurrent {
testDispatcher.scheduler.advanceTimeBy(delayInMillis)
}
companion object {
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

@ -0,0 +1,66 @@
package org.fnives.test.showcase.testutils.idling
import androidx.annotation.CheckResult
import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.junit4.ComposeTestRule
import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.koin.test.KoinTest
class ComposeNetworkSynchronizationTestRule(private val composeTestRule: ComposeTestRule) : TestRule, KoinTest {
private var disposable: Disposable? = null
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
disposable = registerNetworkingSynchronization()
try {
base.evaluate()
} finally {
dispose()
}
}
}
}
fun dispose() = disposable?.dispose()
@CheckResult
private fun registerNetworkingSynchronization(): Disposable {
val idlingResources = NetworkTestConfigurationHelper.getOkHttpClients()
.associateBy(keySelector = { it.toString() })
.map { (key, client) -> OkHttp3IdlingResource.create(key, client) }
.map {
ComposeIdlingResourceDisposable(composeTestRule, object : IdlingResource {
override val isIdleNow: Boolean
get() {
return it.isIdleNow
}
})
}
return CompositeDisposable(idlingResources)
}
}
private class ComposeIdlingResourceDisposable(
private val composeTestRule: ComposeTestRule,
private val idlingResource: IdlingResource
) : Disposable {
override var isDisposed: Boolean = false
private set
init {
composeTestRule.registerIdlingResource(idlingResource)
}
override fun dispose() {
if (isDisposed) return
isDisposed = true
composeTestRule.unregisterIdlingResource(idlingResource)
}
}

View file

@ -22,13 +22,13 @@ class MainDispatcherTestRule : TestRule {
@Throws(Throwable::class) @Throws(Throwable::class)
override fun evaluate() { override fun evaluate() {
val dispatcher = StandardTestDispatcher() val dispatcher = StandardTestDispatcher()
// Dispatchers.setMain(dispatcher) Dispatchers.setMain(dispatcher)
testDispatcher = dispatcher testDispatcher = dispatcher
DatabaseInitialization.dispatcher = dispatcher DatabaseInitialization.dispatcher = dispatcher
try { try {
base.evaluate() base.evaluate()
} finally { } finally {
// Dispatchers.resetMain() Dispatchers.resetMain()
} }
} }
} }

View file

@ -30,7 +30,7 @@ class NetworkSynchronizationTestRule : TestRule, KoinTest {
@CheckResult @CheckResult
private fun registerNetworkingSynchronization(): Disposable { private fun registerNetworkingSynchronization(): Disposable {
val idlingResources = NetworkTestConfigurationHelper.getOkHttpClients()//.filterIndexed { index, okHttpClient -> index == 0 } val idlingResources = NetworkTestConfigurationHelper.getOkHttpClients()
.associateBy(keySelector = { it.toString() }) .associateBy(keySelector = { it.toString() })
.map { (key, client) -> client.asIdlingResource(key) } .map { (key, client) -> client.asIdlingResource(key) }
.map(::IdlingResourceDisposable) .map(::IdlingResourceDisposable)