Issue#106 Move SharedTest Folder into it's separate Android Module as suggested

This commit is contained in:
Gergely Hegedus 2022-09-26 17:25:50 +03:00
parent 9f79b4e67f
commit 9752d1643b
37 changed files with 172 additions and 232 deletions

View file

@ -42,13 +42,11 @@ android {
sourceSets {
androidTest {
java.srcDirs += "src/sharedTest/java"
assets.srcDirs += files("$projectDir/schemas".toString())
// assets.srcDirs += files("$projectDir/schemas".toString())
}
test {
java.srcDirs += "src/sharedTest/java"
java.srcDirs += "src/robolectricTest/java"
resources.srcDirs += files("$projectDir/schemas".toString())
// resources.srcDirs += files("$projectDir/schemas".toString())
}
}
@ -115,6 +113,9 @@ dependencies {
testImplementation testFixtures(project(':core'))
androidTestImplementation testFixtures(project(':core'))
testImplementation project(':app-shared-test')
androidTestImplementation project(':app-shared-test')
// case specific
implementation project(":examplecase:example-navcontroller")
}

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.storage.migration
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MigrationToLatestInstrumentedTest : MigrationToLatestInstrumentedSharedTest()

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.ui.home
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MainActivityInstrumentedTest : MainActivityInstrumentedSharedTest()

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.ui.login
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AuthActivityInstrumentedTest : AuthActivityInstrumentedSharedTest()

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.ui.splash
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SplashActivityInstrumentedTest : SplashActivityInstrumentedSharedTest()

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.storage.migration
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MigrationToLatestInstrumentedTest : MigrationToLatestInstrumentedSharedTest()

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.ui.home
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MainActivityInstrumentedTest : MainActivityInstrumentedSharedTest()

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.ui.login
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class AuthActivityInstrumentedTest : AuthActivityInstrumentedSharedTest()

View file

@ -0,0 +1,7 @@
package org.fnives.test.showcase.ui.splash
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SplashActivityInstrumentedTest : SplashActivityInstrumentedSharedTest()

View file

@ -1,86 +0,0 @@
package org.fnives.test.showcase.storage.migration
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.fnives.test.showcase.android.testutil.SharedMigrationTestRule
import org.fnives.test.showcase.storage.LocalDatabase
import org.fnives.test.showcase.storage.favourite.FavouriteEntity
import org.fnives.test.showcase.storage.migation.Migration1To2
import org.junit.After
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.stopKoin
import java.io.IOException
/**
* reference:
* https://medium.com/androiddevelopers/testing-room-migrations-be93cdb0d975
* https://developer.android.com/training/data-storage/room/migrating-db-versions
*/
@RunWith(AndroidJUnit4::class)
class MigrationToLatestInstrumentedTest {
@get:Rule
val helper = SharedMigrationTestRule<LocalDatabase>(instrumentation = InstrumentationRegistry.getInstrumentation())
private fun getMigratedRoomDatabase(): LocalDatabase {
val database: LocalDatabase = Room.databaseBuilder(
InstrumentationRegistry.getInstrumentation().targetContext,
LocalDatabase::class.java,
TEST_DB
)
.addMigrations(Migration1To2())
.build()
// close the database and release any stream resources when the test finishes
helper.closeWhenFinished(database)
return database
}
@After
fun tearDown() {
stopKoin()
}
@Test
@Throws(IOException::class)
fun migrate1To2() {
val expectedEntities = setOf(
FavouriteEntity("123"),
FavouriteEntity("124"),
FavouriteEntity("125")
)
val version1DB = helper.createDatabase(
name = TEST_DB,
version = 1
)
version1DB.run {
execSQL("INSERT OR IGNORE INTO `FavouriteEntity` (`contentId`) VALUES (\"123\")")
execSQL("INSERT OR IGNORE INTO `FavouriteEntity` (`contentId`) VALUES (124)")
execSQL("INSERT OR IGNORE INTO `FavouriteEntity` (`contentId`) VALUES (125)")
}
version1DB.close()
val version2DB = helper.runMigrationsAndValidate(
name = TEST_DB,
version = 2,
validateDroppedTables = true,
Migration1To2()
)
version2DB.close()
val favouriteDao = getMigratedRoomDatabase().favouriteDao
val entities = runBlocking { favouriteDao.get().first() }.toSet()
Assert.assertEquals(expectedEntities, entities)
}
companion object {
private const val TEST_DB = "migration-test"
}
}

View file

@ -1,49 +0,0 @@
package org.fnives.test.showcase.testutils
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper
import org.fnives.test.showcase.testutils.idling.NetworkSynchronizationTestRule
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.koin.test.KoinTest
/**
* TestRule which ensures Koin is reseted between each tests and setups Network mocking.
*
* It First resets koin if needed.
* Then creates and starts the mockwebserver, it also injects the correct baseUrl into the OkHttp Client.
* Then synchronizes Espresso with the OkHttp Client
*/
class MockServerScenarioSetupResetingTestRule(
private val reloadKoinModulesIfNecessaryTestRule: ReloadKoinModulesIfNecessaryTestRule = ReloadKoinModulesIfNecessaryTestRule(),
private val networkSynchronizationTestRule: TestRule = NetworkSynchronizationTestRule()
) : TestRule, KoinTest {
lateinit var mockServerScenarioSetup: MockServerScenarioSetup
override fun apply(base: Statement, description: Description): Statement =
networkSynchronizationTestRule.apply(base, description)
.let(::createStatement)
.let { reloadKoinModulesIfNecessaryTestRule.apply(it, description) }
private fun createStatement(base: Statement) = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
before()
try {
base.evaluate()
} finally {
after()
}
}
}
private fun before() {
mockServerScenarioSetup = NetworkTestConfigurationHelper.startWithHTTPSMockWebServer()
}
private fun after() {
mockServerScenarioSetup.stop()
}
}

View file

@ -1,64 +0,0 @@
package org.fnives.test.showcase.testutils
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import org.fnives.test.showcase.BuildConfig
import org.fnives.test.showcase.TestShowcaseApplication
import org.fnives.test.showcase.di.createAppModules
import org.fnives.test.showcase.model.network.BaseUrl
import org.fnives.test.showcase.storage.LocalDatabase
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.GlobalContext
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.mp.KoinPlatformTools
import org.koin.test.KoinTest
import org.koin.test.get
/**
* Test rule to help reinitialize the whole Koin setup.
*
* It's needed because in AndroidTest's the Application is only called once,
* meaning our koin would be shared.
*
* 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 {
override fun apply(base: Statement, description: Description): Statement =
ReinitKoinStatement(base)
class ReinitKoinStatement(private val base: Statement) : Statement(), KoinTest {
override fun evaluate() {
reinitKoinIfNeeded()
try {
base.evaluate()
} finally {
closeDB()
stopKoin()
}
}
private fun reinitKoinIfNeeded() {
if (KoinPlatformTools.defaultContext().getOrNull() != null) return
if (GlobalContext.getOrNull() != null) return
val application = ApplicationProvider.getApplicationContext<TestShowcaseApplication>()
val baseUrl = BaseUrl(BuildConfig.BASE_URL)
startKoin {
androidContext(application)
modules(createAppModules(baseUrl))
}
}
private fun closeDB() {
try {
get<LocalDatabase>().close()
} catch (ignored: Throwable) {
Log.d("ReloadKoinModulesRule", "Could not close db: $ignored, stacktrace: ${ignored.stackTraceToString()}")
}
}
}
}

View file

@ -1,32 +0,0 @@
package org.fnives.test.showcase.testutils.idling
import org.fnives.test.showcase.ui.shared.executor.AsyncTaskExecutor
import org.fnives.test.showcase.ui.shared.executor.TaskExecutor
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* Similar Test Rule to InstantTaskExecutorRule just for the [AsyncTaskExecutor] to make AsyncDiffUtil synchronized.
*/
class AsyncDiffUtilInstantTestRule : TestRule {
override fun apply(base: Statement, description: Description): Statement =
object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
AsyncTaskExecutor.delegate = object : TaskExecutor {
override fun executeOnDiskIO(runnable: Runnable) {
runnable.run()
}
override fun postToMainThread(runnable: Runnable) {
runnable.run()
}
}
base.evaluate()
AsyncTaskExecutor.delegate = null
}
}
}

View file

@ -1,52 +0,0 @@
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.android.testutil.synchronization.idlingresources.anyResourceNotIdle
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.awaitIdlingResources
import org.fnives.test.showcase.android.testutil.synchronization.runOnUIAwaitOnCurrent
import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@OptIn(ExperimentalCoroutinesApi::class)
class DatabaseDispatcherTestRule : 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
TestDatabaseInitialization.overwriteDatabaseInitialization(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 (anyResourceNotIdle()) { // 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,14 +0,0 @@
package org.fnives.test.showcase.testutils.idling
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization
import org.fnives.test.showcase.android.testutil.synchronization.MainDispatcherTestRule as LibMainDispatcherTestRule
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherTestRule(useStandard: Boolean = true) : LibMainDispatcherTestRule(useStandard) {
override fun onTestDispatcherInitialized(testDispatcher: TestDispatcher) {
TestDatabaseInitialization.overwriteDatabaseInitialization(testDispatcher)
}
}

View file

@ -1,47 +0,0 @@
package org.fnives.test.showcase.testutils.idling
import androidx.annotation.CheckResult
import androidx.test.espresso.IdlingResource
import okhttp3.OkHttpClient
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.android.testutil.synchronization.idlingresources.IdlingResourceDisposable
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.OkHttp3IdlingResource
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 NetworkSynchronizationTestRule : 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) -> client.asIdlingResource(key) }
.map(::IdlingResourceDisposable)
return CompositeDisposable(idlingResources)
}
private fun OkHttpClient.asIdlingResource(name: String): IdlingResource =
OkHttp3IdlingResource.create(name, this)
}

View file

@ -1,69 +0,0 @@
package org.fnives.test.showcase.testutils.statesetup
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.runner.intent.IntentStubberRegistry
import org.fnives.test.showcase.android.testutil.activity.safeClose
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.home.HomeRobot
import org.fnives.test.showcase.ui.home.MainActivity
import org.fnives.test.showcase.ui.login.LoginRobot
import org.koin.test.KoinTest
object SetupAuthenticationState : KoinTest {
fun setupLogin(
mainDispatcherTestRule: MainDispatcherTestRule,
mockServerScenarioSetup: MockServerScenarioSetup,
resetIntents: Boolean = true,
) {
resetIntentsIfNeeded(resetIntents) {
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b"))
val activityScenario = ActivityScenario.launch(AuthActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
val loginRobot = LoginRobot()
loginRobot.setupIntentResults()
loginRobot
.setPassword("b")
.setUsername("a")
.clickOnLogin()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
activityScenario.safeClose()
}
}
fun setupLogout(
mainDispatcherTestRule: MainDispatcherTestRule,
resetIntents: Boolean = true,
) {
resetIntentsIfNeeded(resetIntents) {
val activityScenario = ActivityScenario.launch(MainActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
val homeRobot = HomeRobot()
homeRobot.setupIntentResults()
homeRobot.clickSignOut()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
activityScenario.safeClose()
}
}
private fun resetIntentsIfNeeded(resetIntents: Boolean, action: () -> Unit) {
val wasInitialized = IntentStubberRegistry.isLoaded()
if (!wasInitialized) {
Intents.init()
}
action()
Intents.release()
if (resetIntents && wasInitialized) {
Intents.init()
}
}
}

View file

@ -1,36 +0,0 @@
package org.fnives.test.showcase.testutils.storage
import android.content.Context
import androidx.room.Room
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asExecutor
import org.fnives.test.showcase.storage.LocalDatabase
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.loadKoinModules
import org.koin.dsl.module
/**
* Reloads the Database Koin module, so it uses the inMemory database with the switched out Executors.
*
* This is needed so in AndroidTests not a real File based device is used.
* This speeds tests up, and isolates them better, there will be no junk in the Database file from previous tests.
*/
object TestDatabaseInitialization {
fun create(context: Context, dispatcher: CoroutineDispatcher): LocalDatabase {
val executor = dispatcher.asExecutor()
return Room.inMemoryDatabaseBuilder(context, LocalDatabase::class.java)
.setTransactionExecutor(executor)
.setQueryExecutor(executor)
.allowMainThreadQueries()
.build()
}
fun overwriteDatabaseInitialization(dispatcher: CoroutineDispatcher) {
loadKoinModules(
module {
single { create(androidContext(), dispatcher) }
}
)
}
}

View file

@ -1,123 +0,0 @@
package org.fnives.test.showcase.ui.home
import android.app.Activity
import android.app.Instrumentation
import android.content.Intent
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasChildCount
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.fnives.test.showcase.R
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.recycler.RemoveItemAnimations
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.FavouriteContent
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.hamcrest.Matchers.allOf
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 assertToolbarIsShown() = apply {
Espresso.onView(withId(R.id.toolbar))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
fun setupIntentResults() {
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
}
fun assertNavigatedToAuth() = apply {
Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
}
fun assertDidNotNavigateToAuth() = apply {
notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
}
fun clickSignOut(setupIntentResults: Boolean = true) = apply {
if (setupIntentResults) {
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
}
Espresso.onView(withId(R.id.logout_cta)).perform(click())
}
fun assertContainsItem(index: Int, item: FavouriteContent) = apply {
removeItemAnimations()
val isFavouriteResourceId = if (item.isFavourite) {
R.drawable.favorite_24
} else {
R.drawable.favorite_border_24
}
Espresso.onView(withId(R.id.recycler))
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(index))
Espresso.onView(
allOf(
withChild(allOf(withText(item.content.title), withId(R.id.title))),
withChild(allOf(withText(item.content.description), withId(R.id.description))),
withChild(allOf(withId(R.id.favourite_cta), WithDrawable(isFavouriteResourceId)))
)
)
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
fun clickOnContentItem(index: Int, item: Content) = apply {
removeItemAnimations()
Espresso.onView(withId(R.id.recycler))
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(index))
Espresso.onView(
allOf(
withId(R.id.favourite_cta),
withParent(
allOf(
withChild(allOf(withText(item.title), withId(R.id.title))),
withChild(allOf(withText(item.description), withId(R.id.description)))
)
)
)
)
.perform(click())
}
fun swipeRefresh() = apply {
Espresso.onView(withId(R.id.swipe_refresh_layout)).perform(PullToRefresh())
}
fun assertContainsNoItems() = apply {
removeItemAnimations()
Espresso.onView(withId(R.id.recycler))
.check(matches(hasChildCount(0)))
}
fun assertContainsError() = apply {
Espresso.onView(withId(R.id.error_message))
.check(matches(allOf(isDisplayed(), withText(R.string.something_went_wrong))))
}
}

View file

@ -1,218 +0,0 @@
package org.fnives.test.showcase.ui.home
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
import org.fnives.test.showcase.android.testutil.activity.safeClose
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.synchronization.loopMainThreadFor
import org.fnives.test.showcase.model.content.FavouriteContent
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.idling.AsyncDiffUtilInstantTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin
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 org.koin.test.KoinTest
@Suppress("TestFunctionName")
@RunWith(AndroidJUnit4::class)
class MainActivityInstrumentedTest : KoinTest {
private lateinit var activityScenario: ActivityScenario<MainActivity>
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
private val mockServerScenarioSetup
get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
private val mainDispatcherTestRule = MainDispatcherTestRule()
private lateinit var robot: HomeRobot
@Rule
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
.around(mockServerScenarioSetupTestRule)
.around(mainDispatcherTestRule)
.around(AsyncDiffUtilInstantTestRule())
.around(SafeCloseActivityRule { activityScenario })
.around(ScreenshotRule("test-showcase"))
@Before
fun setup() {
robot = HomeRobot()
setupLogin(mainDispatcherTestRule, mockServerScenarioSetup)
Intents.init()
}
@After
fun tearDown() {
Intents.release()
}
/** GIVEN initialized MainActivity WHEN signout is clicked THEN user is signed out */
@Test
fun signOutClickedResultsInNavigation() {
mockServerScenarioSetup.setScenario(ContentScenario.Error(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.clickSignOut()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertNavigatedToAuth()
}
/** GIVEN success response WHEN data is returned THEN it is shown on the ui */
@Test
fun successfulDataLoadingShowsTheElementsOnTheUI() {
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
ContentData.contentSuccess.forEachIndexed { index, content ->
robot.assertContainsItem(index, FavouriteContent(content, false))
}
robot.assertDidNotNavigateToAuth()
}
/** GIVEN success response WHEN item is clicked THEN ui is updated */
@Test
fun clickingOnListElementUpdatesTheElementsFavouriteState() {
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.clickOnContentItem(0, ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true)
robot.assertContainsItem(0, expectedItem)
.assertDidNotNavigateToAuth()
}
/** GIVEN success response WHEN item is clicked THEN ui is updated even if activity is recreated */
@Test
fun elementFavouritedIsKeptEvenIfActivityIsRecreated() {
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.clickOnContentItem(0, ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), true)
activityScenario.safeClose()
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertContainsItem(0, expectedItem)
.assertDidNotNavigateToAuth()
}
/** GIVEN success response WHEN item is clicked then clicked again THEN ui is updated */
@Test
fun clickingAnElementMultipleTimesProperlyUpdatesIt() {
mockServerScenarioSetup.setScenario(ContentScenario.Success(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.clickOnContentItem(0, ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.clickOnContentItem(0, ContentData.contentSuccess.first())
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
val expectedItem = FavouriteContent(ContentData.contentSuccess.first(), false)
robot.assertContainsItem(0, expectedItem)
.assertDidNotNavigateToAuth()
}
/** GIVEN error response WHEN loaded THEN error is Shown */
@Test
fun networkErrorResultsInUIErrorStateShown() {
mockServerScenarioSetup.setScenario(ContentScenario.Error(usingRefreshedToken = false))
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertContainsNoItems()
.assertContainsError()
.assertDidNotNavigateToAuth()
}
/** GIVEN error response then success WHEN retried THEN success is shown */
@Test
fun retryingFromErrorStateAndSucceedingShowsTheData() {
mockServerScenarioSetup.setScenario(
ContentScenario.Error(usingRefreshedToken = false)
.then(ContentScenario.Success(usingRefreshedToken = false))
)
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.swipeRefresh()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
loopMainThreadFor(2000L)
ContentData.contentSuccess.forEachIndexed { index, content ->
robot.assertContainsItem(index, FavouriteContent(content, false))
}
robot.assertDidNotNavigateToAuth()
}
/** GIVEN success then error WHEN retried THEN error is shown */
@Test
fun errorIsShownIfTheDataIsFetchedAndErrorIsReceived() {
mockServerScenarioSetup.setScenario(
ContentScenario.Success(usingRefreshedToken = false)
.then(ContentScenario.Error(usingRefreshedToken = false))
)
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.swipeRefresh()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot
.assertContainsError()
.assertContainsNoItems()
.assertDidNotNavigateToAuth()
}
/** GIVEN unauthenticated then success WHEN loaded THEN success is shown */
@Test
fun authenticationIsHandledWithASingleLoading() {
mockServerScenarioSetup.setScenario(
ContentScenario.Unauthorized(usingRefreshedToken = false)
.then(ContentScenario.Success(usingRefreshedToken = true))
)
.setScenario(RefreshTokenScenario.Success)
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
ContentData.contentSuccess.forEachIndexed { index, content ->
robot.assertContainsItem(index, FavouriteContent(content, false))
}
robot.assertDidNotNavigateToAuth()
}
/** GIVEN unauthenticated then error WHEN loaded THEN navigated to auth */
@Test
fun sessionExpirationResultsInNavigation() {
mockServerScenarioSetup.setScenario(ContentScenario.Unauthorized(usingRefreshedToken = false))
.setScenario(RefreshTokenScenario.Error)
activityScenario = ActivityScenario.launch(MainActivity::class.java)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertNavigatedToAuth()
}
}

View file

@ -1,144 +0,0 @@
package org.fnives.test.showcase.ui.login
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fnives.test.showcase.R
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
import org.fnives.test.showcase.ui.auth.AuthActivity
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 org.koin.test.KoinTest
@Suppress("TestFunctionName")
@RunWith(AndroidJUnit4::class)
class AuthActivityInstrumentedTest : KoinTest {
private lateinit var activityScenario: ActivityScenario<AuthActivity>
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
private val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
private val mainDispatcherTestRule = MainDispatcherTestRule()
private lateinit var robot: LoginRobot
@Rule
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
.around(mockServerScenarioSetupTestRule)
.around(mainDispatcherTestRule)
.around(SafeCloseActivityRule { activityScenario })
.around(ScreenshotRule("test-showcase"))
@Before
fun setup() {
Intents.init()
robot = LoginRobot()
}
@After
fun tearDown() {
Intents.release()
}
/** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */
@Test
fun properLoginResultsInNavigationToHome() {
mockServerScenarioSetup.setScenario(
AuthScenario.Success(password = "alma", username = "banan")
)
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
robot
.setPassword("alma")
.setUsername("banan")
.assertPassword("alma")
.assertUsername("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertNavigatedToHome()
}
/** GIVEN empty password and username WHEN signIn THEN error password is shown */
@Test
fun emptyPasswordShowsProperErrorMessage() {
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
robot
.setUsername("banan")
.assertUsername("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertErrorIsShown(R.string.password_is_invalid)
.assertNotNavigatedToHome()
.assertNotLoading()
}
/** GIVEN password and empty username WHEN signIn THEN error username is shown */
@Test
fun emptyUserNameShowsProperErrorMessage() {
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
robot
.setPassword("banan")
.assertPassword("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertErrorIsShown(R.string.username_is_invalid)
.assertNotNavigatedToHome()
.assertNotLoading()
}
/** GIVEN password and username and invalid credentials response WHEN signIn THEN error invalid credentials is shown */
@Test
fun invalidCredentialsGivenShowsProperErrorMessage() {
mockServerScenarioSetup.setScenario(
AuthScenario.InvalidCredentials(username = "alma", password = "banan")
)
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
robot
.setUsername("alma")
.setPassword("banan")
.assertUsername("alma")
.assertPassword("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertErrorIsShown(R.string.credentials_invalid)
.assertNotNavigatedToHome()
.assertNotLoading()
}
/** GIVEN password and username and error response WHEN signIn THEN error invalid credentials is shown */
@Test
fun networkErrorShowsProperErrorMessage() {
mockServerScenarioSetup.setScenario(
AuthScenario.GenericError(username = "alma", password = "banan")
)
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
robot
.setUsername("alma")
.setPassword("banan")
.assertUsername("alma")
.assertPassword("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
robot.assertErrorIsShown(R.string.something_went_wrong)
.assertNotNavigatedToHome()
.assertNotLoading()
}
}

View file

@ -1,94 +0,0 @@
package org.fnives.test.showcase.ui.login
import android.app.Activity
import android.app.Instrumentation
import android.content.Intent
import androidx.annotation.StringRes
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
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.android.testutil.intent.notIntended
import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsNotShown
import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsShownWithText
import org.fnives.test.showcase.android.testutil.viewaction.progressbar.ReplaceProgressBarDrawableToStatic
import org.fnives.test.showcase.ui.home.MainActivity
import org.hamcrest.core.IsNot.not
class LoginRobot {
fun setupIntentResults() {
Intents.intending(hasComponent(MainActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
}
/**
* 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())
}
fun setPassword(password: String): LoginRobot = apply {
onView(withId(R.id.password_edit_text))
.perform(ViewActions.replaceText(password), ViewActions.closeSoftKeyboard())
}
fun clickOnLogin() = apply {
replaceProgressBar()
onView(withId(R.id.login_cta))
.perform(ViewActions.click())
}
fun assertPassword(password: String) = apply {
onView(withId((R.id.password_edit_text)))
.check(ViewAssertions.matches(ViewMatchers.withText(password)))
}
fun assertUsername(username: String) = apply {
onView(withId((R.id.user_edit_text)))
.check(ViewAssertions.matches(ViewMatchers.withText(username)))
}
fun assertErrorIsShown(@StringRes stringResID: Int) = apply {
assertSnackBarIsShownWithText(stringResID)
}
fun assertLoadingBeforeRequests() = apply {
onView(withId(R.id.loading_indicator))
.check(ViewAssertions.matches(isDisplayed()))
}
fun assertNotLoading() = apply {
onView(withId(R.id.loading_indicator))
.check(ViewAssertions.matches(not(isDisplayed())))
}
fun assertErrorIsNotShown() = apply {
assertSnackBarIsNotShown()
}
fun assertNavigatedToHome() = apply {
intended(hasComponent(MainActivity::class.java.canonicalName))
}
fun assertNotNavigatedToHome() = apply {
notIntended(hasComponent(MainActivity::class.java.canonicalName))
}
}

View file

@ -1,51 +0,0 @@
package org.fnives.test.showcase.ui.login.codekata
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.GlobalContext
import org.koin.test.KoinTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
@Ignore("CodeKata")
class CodeKataAuthActivitySharedTest : KoinTest {
@Before
fun setup() {
}
@After
fun tearDown() {
GlobalContext.stopKoin()
}
/** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */
@Test
fun properLoginResultsInNavigationToHome() {
}
/** GIVEN empty password and username WHEN signIn THEN error password is shown */
@Test
fun emptyPasswordShowsProperErrorMessage() {
}
/** GIVEN password and empty username WHEN signIn THEN error username is shown */
@Test
fun emptyUserNameShowsProperErrorMessage() {
}
/** GIVEN password and username and invalid credentials response WHEN signIn THEN error invalid credentials is shown */
@Test
fun invalidCredentialsGivenShowsProperErrorMessage() {
}
/** GIVEN password and username and error response WHEN signIn THEN error invalid credentials is shown */
@Test
fun networkErrorShowsProperErrorMessage() {
}
}

View file

@ -1,69 +0,0 @@
package org.fnives.test.showcase.ui.login.codekata
import androidx.annotation.StringRes
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers
import org.fnives.test.showcase.R
import org.fnives.test.showcase.android.testutil.intent.notIntended
import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsNotShown
import org.fnives.test.showcase.android.testutil.snackbar.SnackbarVerificationHelper.assertSnackBarIsShownWithText
import org.fnives.test.showcase.ui.home.MainActivity
import org.hamcrest.core.IsNot
class CodeKataSharedRobotTest {
fun setUsername(username: String): CodeKataSharedRobotTest = apply {
Espresso.onView(ViewMatchers.withId(R.id.user_edit_text))
.perform(ViewActions.replaceText(username), ViewActions.closeSoftKeyboard())
}
fun setPassword(password: String): CodeKataSharedRobotTest = apply {
Espresso.onView(ViewMatchers.withId(R.id.password_edit_text))
.perform(ViewActions.replaceText(password), ViewActions.closeSoftKeyboard())
}
fun clickOnLogin(): CodeKataSharedRobotTest = apply {
Espresso.onView(ViewMatchers.withId(R.id.login_cta))
.perform(ViewActions.click())
}
fun assertPassword(password: String): CodeKataSharedRobotTest = apply {
Espresso.onView(ViewMatchers.withId((R.id.password_edit_text)))
.check(ViewAssertions.matches(ViewMatchers.withText(password)))
}
fun assertUsername(username: String): CodeKataSharedRobotTest = apply {
Espresso.onView(ViewMatchers.withId((R.id.user_edit_text)))
.check(ViewAssertions.matches(ViewMatchers.withText(username)))
}
fun assertLoadingBeforeRequests(): CodeKataSharedRobotTest = apply {
Espresso.onView(ViewMatchers.withId(R.id.loading_indicator))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
fun assertNotLoading(): CodeKataSharedRobotTest = apply {
Espresso.onView(ViewMatchers.withId(R.id.loading_indicator))
.check(ViewAssertions.matches(IsNot.not(ViewMatchers.isDisplayed())))
}
fun assertErrorIsShown(@StringRes stringResID: Int): CodeKataSharedRobotTest = apply {
assertSnackBarIsShownWithText(stringResID)
}
fun assertErrorIsNotShown(): CodeKataSharedRobotTest = apply {
assertSnackBarIsNotShown()
}
fun assertNavigatedToHome(): CodeKataSharedRobotTest = apply {
Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
}
fun assertNotNavigatedToHome(): CodeKataSharedRobotTest = apply {
notIntended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
}
}

View file

@ -1,16 +0,0 @@
package org.fnives.test.showcase.ui.login.codekata.rule.dispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@OptIn(ExperimentalCoroutinesApi::class)
class CodeKataMainDispatcherRule : TestRule {
override fun apply(base: Statement, description: Description): Statement =
object : Statement() {
override fun evaluate() {
TODO("Not yet implemented")
}
}
}

View file

@ -1,41 +0,0 @@
package org.fnives.test.showcase.ui.login.codekata.rule.dispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.fnives.test.showcase.storage.database.DatabaseInitialization
import org.fnives.test.showcase.testutils.storage.TestDatabaseInitialization
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* Sets up the Dispatcher as Main and as the [DatabaseInitialization]'s dispatcher.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class PlainMainDispatcherRule(private val useStandard: Boolean = true) : TestRule {
private var _testDispatcher: TestDispatcher? = null
val testDispatcher
get() = _testDispatcher
?: throw IllegalStateException("TestDispatcher is accessed before it is initialized!")
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
override fun evaluate() {
try {
val dispatcher = if (useStandard) StandardTestDispatcher() else UnconfinedTestDispatcher()
Dispatchers.setMain(dispatcher)
TestDatabaseInitialization.overwriteDatabaseInitialization(dispatcher)
_testDispatcher = dispatcher
base.evaluate()
} finally {
_testDispatcher = null
Dispatchers.resetMain()
}
}
}
}

View file

@ -1,11 +0,0 @@
package org.fnives.test.showcase.ui.login.codekata.rule.intent
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
class CodeKataIntentInitRule : TestRule {
override fun apply(base: Statement, description: Description): Statement {
TODO()
}
}

View file

@ -1,25 +0,0 @@
package org.fnives.test.showcase.ui.login.codekata.rule.intent
import androidx.test.espresso.intent.Intents
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* Takes care of [Intents] initialization.
*/
class PlainIntentInitRule : TestRule {
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
try {
Intents.init()
base.evaluate()
} finally {
Intents.release()
}
}
}
}
}

View file

@ -1,105 +0,0 @@
package org.fnives.test.showcase.ui.splash
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
import org.fnives.test.showcase.android.testutil.intent.DismissSystemDialogsRule
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogout
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 org.koin.test.KoinTest
@Suppress("TestFunctionName")
@RunWith(AndroidJUnit4::class)
class SplashActivityInstrumentedTest : KoinTest {
private lateinit var activityScenario: ActivityScenario<SplashActivity>
private val mainDispatcherTestRule = MainDispatcherTestRule()
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
private lateinit var robot: SplashRobot
@Rule
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(DismissSystemDialogsRule())
.around(mockServerScenarioSetupTestRule)
.around(mainDispatcherTestRule)
.around(SafeCloseActivityRule { activityScenario })
.around(ScreenshotRule("test-showcase"))
@Before
fun setup() {
Intents.init()
robot = SplashRobot()
}
@After
fun tearDown() {
Intents.release()
}
/** GIVEN loggedInState WHEN opened after some time THEN MainActivity is started */
@Test
fun loggedInStateNavigatesToHome() {
setupLogin(mainDispatcherTestRule, mockServerScenarioSetupTestRule.mockServerScenarioSetup)
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
mainDispatcherTestRule.advanceTimeBy(501)
robot.assertHomeIsStarted()
.assertAuthIsNotStarted()
}
/** GIVEN loggedOffState WHEN opened after some time THEN AuthActivity is started */
@Test
fun loggedOutStatesNavigatesToAuthentication() {
setupLogout(mainDispatcherTestRule)
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
mainDispatcherTestRule.advanceTimeBy(501)
robot.assertAuthIsStarted()
.assertHomeIsNotStarted()
}
/** GIVEN loggedOffState and not enough time WHEN opened THEN no activity is started */
@Test
fun loggedOutStatesNotEnoughTime() {
setupLogout(mainDispatcherTestRule)
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
mainDispatcherTestRule.advanceTimeBy(500)
robot.assertAuthIsNotStarted()
.assertHomeIsNotStarted()
}
/** GIVEN loggedInState and not enough time WHEN opened THEN no activity is started */
@Test
fun loggedInStatesNotEnoughTime() {
setupLogin(mainDispatcherTestRule, mockServerScenarioSetupTestRule.mockServerScenarioSetup)
activityScenario = ActivityScenario.launch(SplashActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
mainDispatcherTestRule.advanceTimeBy(500)
robot.assertHomeIsNotStarted()
.assertAuthIsNotStarted()
}
}

View file

@ -1,36 +0,0 @@
package org.fnives.test.showcase.ui.splash
import android.app.Activity
import android.app.Instrumentation
import android.content.Intent
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import org.fnives.test.showcase.android.testutil.intent.notIntended
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.home.MainActivity
class SplashRobot {
fun setupIntentResults() {
Intents.intending(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
}
fun assertHomeIsStarted() = apply {
Intents.intended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
}
fun assertHomeIsNotStarted() = apply {
notIntended(IntentMatchers.hasComponent(MainActivity::class.java.canonicalName))
}
fun assertAuthIsStarted() = apply {
Intents.intended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
}
fun assertAuthIsNotStarted() = apply {
notIntended(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
}
}