Issue#13 Add CodeKata for Robolectric Tests

This commit is contained in:
Gergely Hegedus 2022-01-28 23:48:49 +02:00
parent c38e608c8c
commit 03e413fba6
25 changed files with 758 additions and 296 deletions

View file

@ -1,44 +0,0 @@
package org.fnives.test.showcase.testutils.configuration
import android.view.View
import androidx.annotation.StringRes
import androidx.test.espresso.Espresso
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import com.google.android.material.R
import com.google.android.material.snackbar.Snackbar
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.junit.runner.Description
import org.junit.runners.model.Statement
object AndroidTestSnackbarVerificationHelper : SnackbarVerificationHelper {
override fun apply(base: Statement, description: Description): Statement = base
override fun assertIsShownWithText(@StringRes stringResID: Int) {
Espresso.onView(ViewMatchers.withId(R.id.snackbar_text))
.check(ViewAssertions.matches(ViewMatchers.withText(stringResID)))
Espresso.onView(ViewMatchers.isAssignableFrom(Snackbar.SnackbarLayout::class.java)).perform(ViewActions.swipeRight())
Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainUntilSnackbarDismissed())
}
override fun assertIsNotShown() {
Espresso.onView(ViewMatchers.withId(R.id.snackbar_text)).check(ViewAssertions.doesNotExist())
}
class LoopMainUntilSnackbarDismissed() : ViewAction {
override fun getConstraints(): Matcher<View> = Matchers.isA(View::class.java)
override fun getDescription(): String = "loop MainThread until Snackbar is Dismissed"
override fun perform(uiController: UiController, view: View?) {
while (view?.findViewById<View>(com.google.android.material.R.id.snackbar_text) != null) {
uiController.loopMainThreadForAtLeast(100)
}
}
}
}

View file

@ -2,9 +2,6 @@ package org.fnives.test.showcase.testutils.configuration
object SpecificTestConfigurationsFactory : TestConfigurationsFactory {
override fun createSnackbarVerification(): SnackbarVerificationHelper =
AndroidTestSnackbarVerificationHelper
override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory =
AndroidMigrationTestRuleFactory
}

View file

@ -1,19 +0,0 @@
package org.fnives.test.showcase.testutils.configuration
import androidx.annotation.StringRes
import org.fnives.test.showcase.testutils.shadow.ShadowSnackbar
import org.fnives.test.showcase.testutils.shadow.ShadowSnackbarResetTestRule
import org.junit.Assert
import org.junit.rules.TestRule
object RobolectricSnackbarVerificationHelper : SnackbarVerificationHelper, TestRule by ShadowSnackbarResetTestRule() {
override fun assertIsShownWithText(@StringRes stringResID: Int) {
val latestSnackbar = ShadowSnackbar.latestSnackbar ?: throw IllegalStateException("Snackbar not found")
Assert.assertEquals(latestSnackbar.context.getString(stringResID), ShadowSnackbar.textOfLatestSnackbar)
}
override fun assertIsNotShown() {
Assert.assertNull(ShadowSnackbar.latestSnackbar)
}
}

View file

@ -2,9 +2,6 @@ package org.fnives.test.showcase.testutils.configuration
object SpecificTestConfigurationsFactory : TestConfigurationsFactory {
override fun createSnackbarVerification(): SnackbarVerificationHelper =
RobolectricSnackbarVerificationHelper
override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory =
RobolectricMigrationTestHelperFactory
}

View file

@ -1,100 +0,0 @@
package org.fnives.test.showcase.testutils.shadow
import android.R
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.annotation.StringRes
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.ContentViewCallback
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.SnackbarContentLayout
import org.robolectric.annotation.Implementation
import org.robolectric.annotation.Implements
import org.robolectric.annotation.RealObject
import org.robolectric.shadow.api.Shadow.extract
import java.lang.reflect.Modifier
@Implements(Snackbar::class)
class ShadowSnackbar {
@RealObject
var snackbar: Snackbar? = null
var text: String? = null
companion object {
val shadowSnackbars = mutableListOf<ShadowSnackbar>()
@Implementation
@JvmStatic
fun make(view: View, text: CharSequence, duration: Int): Snackbar? {
val snackbar: Snackbar?
try {
val constructor = Snackbar::class.java.getDeclaredConstructor(
Context::class.java,
ViewGroup::class.java,
View::class.java,
ContentViewCallback::class.java
) ?: throw IllegalArgumentException("Seems like the constructor was not found!")
if (Modifier.isPrivate(constructor.modifiers)) {
constructor.isAccessible = true
}
val parent = findSuitableParent(view)
val content = LayoutInflater.from(parent.context)
.inflate(
com.google.android.material.R.layout.design_layout_snackbar_include,
parent,
false
) as SnackbarContentLayout
snackbar = constructor.newInstance(view.context, parent, content, content)
snackbar.setText(text)
snackbar.duration = duration
} catch (e: Exception) {
e.printStackTrace()
throw e
}
shadowOf(snackbar).text = text.toString()
shadowSnackbars.add(shadowOf(snackbar))
return snackbar
}
private fun findSuitableParent(view: View): ViewGroup =
when (view) {
is CoordinatorLayout -> view
is FrameLayout -> {
when {
view.id == R.id.content -> view
(view.parent as? View) == null -> view
else -> findSuitableParent(view.parent as View)
}
}
else -> {
when {
(view.parent as? View) == null && view is ViewGroup -> view
(view.parent as? View) == null -> FrameLayout(view.context)
else -> findSuitableParent(view.parent as View)
}
}
}
@Implementation
@JvmStatic
fun make(view: View, @StringRes resId: Int, duration: Int): Snackbar? =
make(view, view.resources.getText(resId), duration)
fun shadowOf(bar: Snackbar?): ShadowSnackbar =
extract(bar)
fun reset() {
shadowSnackbars.clear()
}
fun shownSnackbarCount(): Int = shadowSnackbars.size
val textOfLatestSnackbar: String?
get() = shadowSnackbars.lastOrNull()?.text
val latestSnackbar: Snackbar?
get() = shadowSnackbars.lastOrNull()?.snackbar
}
}

View file

@ -1,20 +0,0 @@
package org.fnives.test.showcase.testutils.shadow
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
class ShadowSnackbarResetTestRule : TestRule {
override fun apply(base: Statement, description: Description): Statement =
object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
ShadowSnackbar.reset()
try {
base.evaluate()
} finally {
ShadowSnackbar.reset()
}
}
}
}

View file

@ -0,0 +1,166 @@
package org.fnives.test.showcase.ui
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.fnives.test.showcase.R
import org.fnives.test.showcase.network.mockserver.MockServerScenarioSetup
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
import org.fnives.test.showcase.network.testutil.NetworkTestConfigurationHelper
import org.fnives.test.showcase.storage.database.DatabaseInitialization
import org.fnives.test.showcase.testutils.idling.CompositeDisposable
import org.fnives.test.showcase.testutils.idling.Disposable
import org.fnives.test.showcase.testutils.idling.IdlingResourceDisposable
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule.Companion.advanceUntilIdleWithIdlingResources
import org.fnives.test.showcase.testutils.idling.OkHttp3IdlingResource
import org.fnives.test.showcase.testutils.safeClose
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.GlobalContext.stopKoin
import org.koin.test.KoinTest
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class RobolectricAuthActivityInstrumentedTest : KoinTest {
private lateinit var activityScenario: ActivityScenario<AuthActivity>
private lateinit var robot: RobolectricLoginRobot
private lateinit var testDispatcher: TestDispatcher
private lateinit var mockServerScenarioSetup: MockServerScenarioSetup
private lateinit var disposable: Disposable
@Before
fun setup() {
Intents.init()
val dispatcher = StandardTestDispatcher(TestCoroutineScheduler())
Dispatchers.setMain(dispatcher)
testDispatcher = dispatcher
DatabaseInitialization.dispatcher = dispatcher
mockServerScenarioSetup = NetworkTestConfigurationHelper.startWithHTTPSMockWebServer()
val idlingResources = NetworkTestConfigurationHelper.getOkHttpClients()
.associateBy(keySelector = { it.toString() })
.map { (key, client) -> OkHttp3IdlingResource.create(key, client) }
.map(::IdlingResourceDisposable)
disposable = CompositeDisposable(idlingResources)
robot = RobolectricLoginRobot()
activityScenario = ActivityScenario.launch(AuthActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
}
@After
fun tearDown() {
stopKoin()
Dispatchers.resetMain()
mockServerScenarioSetup.stop()
disposable.dispose()
activityScenario.safeClose()
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"),
validateArguments = true
)
robot.setPassword("alma")
.setUsername("banan")
.assertPassword("alma")
.assertUsername("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
.assertErrorIsNotShown()
testDispatcher.advanceUntilIdleWithIdlingResources()
robot.assertNavigatedToHome()
}
/** GIVEN empty password and username WHEN signIn THEN error password is shown */
@Test
fun emptyPasswordShowsProperErrorMessage() {
robot.setUsername("banan")
.assertUsername("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
.assertErrorIsNotShown()
testDispatcher.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() {
robot.setPassword("banan")
.assertPassword("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
testDispatcher.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"),
validateArguments = true
)
robot
.setUsername("alma")
.setPassword("banan")
.assertUsername("alma")
.assertPassword("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
.assertErrorIsNotShown()
testDispatcher.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"),
validateArguments = true
)
robot
.setUsername("alma")
.setPassword("banan")
.assertUsername("alma")
.assertPassword("banan")
.clickOnLogin()
.assertLoadingBeforeRequests()
.assertErrorIsNotShown()
testDispatcher.advanceUntilIdleWithIdlingResources()
robot.assertErrorIsShown(R.string.something_went_wrong)
.assertNotNavigatedToHome()
.assertNotLoading()
}
}

View file

@ -0,0 +1,74 @@
package org.fnives.test.showcase.ui
//import org.fnives.test.showcase.testutils.shadow.ShadowSnackbar
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.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.testutils.configuration.SnackbarVerificationHelper
import org.fnives.test.showcase.testutils.viewactions.notIntended
import org.fnives.test.showcase.ui.home.MainActivity
import org.hamcrest.core.IsNot.not
class RobolectricLoginRobot(
private val snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationHelper()
) {
fun setUsername(username: String): RobolectricLoginRobot = apply {
onView(withId(R.id.user_edit_text))
.perform(ViewActions.replaceText(username), ViewActions.closeSoftKeyboard())
}
fun setPassword(password: String): RobolectricLoginRobot = apply {
onView(withId(R.id.password_edit_text))
.perform(ViewActions.replaceText(password), ViewActions.closeSoftKeyboard())
}
fun clickOnLogin() = apply {
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 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 assertErrorIsShown(@StringRes stringResID: Int) = apply {
snackbarVerificationHelper.assertIsShownWithText(stringResID)
}
fun assertErrorIsNotShown() = apply {
snackbarVerificationHelper.assertIsNotShown()
}
fun assertNavigatedToHome() = apply {
intended(hasComponent(MainActivity::class.java.canonicalName))
}
fun assertNotNavigatedToHome() = apply {
notIntended(hasComponent(MainActivity::class.java.canonicalName))
}
}

View file

@ -0,0 +1,51 @@
package org.fnives.test.showcase.ui.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.stopKoin
import org.koin.test.KoinTest
@Ignore("CodeKata")
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class CodeKataAuthActivityInstrumentedTest : KoinTest {
@Before
fun setup() {
}
@After
fun tearDown() {
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

@ -0,0 +1,4 @@
package org.fnives.test.showcase.ui.codekata
class CodeKataLoginRobot {
}

View file

@ -1,15 +1,40 @@
package org.fnives.test.showcase.testutils.configuration
import android.view.View
import androidx.annotation.StringRes
import org.junit.rules.TestRule
import androidx.test.espresso.Espresso
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import com.google.android.material.R
import com.google.android.material.snackbar.Snackbar
import org.hamcrest.Matcher
import org.hamcrest.Matchers
interface SnackbarVerificationHelper : TestRule {
class SnackbarVerificationHelper {
fun assertIsShownWithText(@StringRes stringResID: Int)
fun assertIsShownWithText(@StringRes stringResID: Int) {
Espresso.onView(ViewMatchers.withId(R.id.snackbar_text))
.check(ViewAssertions.matches(ViewMatchers.withText(stringResID)))
Espresso.onView(ViewMatchers.isAssignableFrom(Snackbar.SnackbarLayout::class.java)).perform(ViewActions.swipeRight())
Espresso.onView(ViewMatchers.isRoot()).perform(LoopMainUntilSnackbarDismissed())
}
fun assertIsNotShown()
fun assertIsNotShown() {
Espresso.onView(ViewMatchers.withId(R.id.snackbar_text)).check(ViewAssertions.doesNotExist())
}
class LoopMainUntilSnackbarDismissed : ViewAction {
override fun getConstraints(): Matcher<View> = Matchers.isA(View::class.java)
override fun getDescription(): String = "loop MainThread until Snackbar is Dismissed"
override fun perform(uiController: UiController, view: View?) {
while (view?.findViewById<View>(com.google.android.material.R.id.snackbar_text) != null) {
uiController.loopMainThreadForAtLeast(100)
}
}
}
}
@Suppress("TestFunctionName")
fun SnackbarVerificationTestRule(): SnackbarVerificationHelper =
SpecificTestConfigurationsFactory.createSnackbarVerification()

View file

@ -8,7 +8,5 @@ package org.fnives.test.showcase.testutils.configuration
*/
interface TestConfigurationsFactory {
fun createSnackbarVerification(): SnackbarVerificationHelper
fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory
}

View file

@ -46,12 +46,14 @@ class MainDispatcherTestRule : TestRule {
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
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()
}
scheduler.advanceUntilIdle()
}
}

View file

@ -1,8 +0,0 @@
package org.fnives.test.showcase.testutils.robot
interface Robot {
fun init()
fun release()
}

View file

@ -1,20 +0,0 @@
package org.fnives.test.showcase.testutils.robot
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
class RobotTestRule<T : Robot>(val robot: T) : TestRule {
override fun apply(base: Statement, description: Description): Statement =
object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
robot.init()
try {
base.evaluate()
} finally {
robot.release()
}
}
}
}

View file

@ -21,38 +21,48 @@ object SetupAuthenticationState : KoinTest {
mockServerScenarioSetup: MockServerScenarioSetup,
resetIntents: Boolean = true
) {
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()
resetIntentsIfNeeded(resetIntents) {
mockServerScenarioSetup.setScenario(AuthScenario.Success(username = "a", password = "b"))
val activityScenario = ActivityScenario.launch(AuthActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
val loginRobot = LoginRobot()
loginRobot.setupIntentResults()
loginRobot
.setPassword("b")
.setUsername("a")
.clickOnLogin()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
activityScenario.safeClose()
resetIntentsIfNeeded(resetIntents)
activityScenario.safeClose()
}
}
fun setupLogout(
mainDispatcherTestRule: MainDispatcherTestRule,
resetIntents: Boolean = true
) {
val activityScenario = ActivityScenario.launch(MainActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
HomeRobot().clickSignOut()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
resetIntentsIfNeeded(resetIntents) {
val activityScenario = ActivityScenario.launch(MainActivity::class.java)
activityScenario.moveToState(Lifecycle.State.RESUMED)
activityScenario.safeClose()
resetIntentsIfNeeded(resetIntents)
val homeRobot = HomeRobot()
homeRobot.setupIntentResults()
homeRobot.clickSignOut()
mainDispatcherTestRule.advanceUntilIdleWithIdlingResources()
activityScenario.safeClose()
}
}
private fun resetIntentsIfNeeded(resetIntents: Boolean) {
if (resetIntents && IntentStubberRegistry.isLoaded()) {
Intents.release()
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,6 +1,8 @@
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
@ -19,21 +21,17 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import org.fnives.test.showcase.R
import org.fnives.test.showcase.model.content.Content
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.PullToRefresh
import org.fnives.test.showcase.testutils.viewactions.WithDrawable
import org.fnives.test.showcase.testutils.viewactions.notIntended
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.hamcrest.Matchers.allOf
class HomeRobot : Robot {
class HomeRobot {
override fun init() {
Intents.init()
}
override fun release() {
Intents.release()
fun setupIntentResults() {
Intents.intending(IntentMatchers.hasComponent(AuthActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
}
fun assertNavigatedToAuth() = apply {

View file

@ -1,6 +1,7 @@
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.model.content.FavouriteContent
import org.fnives.test.showcase.network.mockserver.ContentData
@ -10,7 +11,6 @@ import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRul
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
@ -31,22 +31,24 @@ class MainActivityInstrumentedTest : KoinTest {
private val mockServerScenarioSetup
get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
private val mainDispatcherTestRule = MainDispatcherTestRule()
private val robot = HomeRobot()
private lateinit var robot : HomeRobot
@Rule
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
.around(mainDispatcherTestRule)
.around(RobotTestRule(robot))
@Before
fun setup() {
robot = HomeRobot()
setupLogin(mainDispatcherTestRule, mockServerScenarioSetup)
Intents.init()
}
@After
fun tearDown() {
activityScenario.safeClose()
Intents.release()
}
/** GIVEN initialized MainActivity WHEN signout is clicked THEN user is signed out */

View file

@ -1,15 +1,16 @@
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.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.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.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@ -25,17 +26,23 @@ class AuthActivityInstrumentedTest : KoinTest {
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
private val mockServerScenarioSetup get() = mockServerScenarioSetupTestRule.mockServerScenarioSetup
private val mainDispatcherTestRule = MainDispatcherTestRule()
private val robot = LoginRobot()
private lateinit var robot : LoginRobot
@Rule
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
.around(mainDispatcherTestRule)
.around(RobotTestRule(robot))
@Before
fun setup() {
Intents.init()
robot = LoginRobot()
}
@After
fun tearDown() {
activityScenario.safeClose()
Intents.release()
}
/** GIVEN non empty password and username and successful response WHEN signIn THEN no error is shown and navigating to home */

View file

@ -9,38 +9,26 @@ 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.Intents.intending
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.testutils.configuration.SnackbarVerificationHelper
import org.fnives.test.showcase.testutils.configuration.SnackbarVerificationTestRule
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 snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationTestRule()
) : Robot {
override fun init() {
Intents.init()
setupIntentResults()
}
private val snackbarVerificationHelper: SnackbarVerificationHelper = SnackbarVerificationHelper()
){
fun setupIntentResults() {
intending(hasComponent(MainActivity::class.java.canonicalName))
Intents.intending(hasComponent(MainActivity::class.java.canonicalName))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()))
}
override fun release() {
Intents.release()
}
/**
* Needed because Espresso idling waits until mainThread is idle.
*

View file

@ -2,14 +2,15 @@ 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.testutils.MockServerScenarioSetupResetingTestRule
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
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@ -25,17 +26,23 @@ class SplashActivityInstrumentedTest : KoinTest {
private val mainDispatcherTestRule = MainDispatcherTestRule()
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
private val robot = SplashRobot()
private lateinit var robot : SplashRobot
@Rule
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
.around(mainDispatcherTestRule)
.around(RobotTestRule(robot))
@Before
fun setup() {
Intents.init()
robot = SplashRobot()
}
@After
fun tearDown() {
activityScenario.safeClose()
Intents.release()
}
/** GIVEN loggedInState WHEN opened after some time THEN MainActivity is started */

View file

@ -1,20 +1,21 @@
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.testutils.robot.Robot
import org.fnives.test.showcase.testutils.viewactions.notIntended
import org.fnives.test.showcase.ui.auth.AuthActivity
import org.fnives.test.showcase.ui.home.MainActivity
class SplashRobot : Robot {
class SplashRobot {
override fun init() {
Intents.init()
}
override fun release() {
Intents.release()
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 {

View file

@ -1,3 +1,3 @@
sdk=28
shadows=org.fnives.test.showcase.testutils.shadow.ShadowSnackbar
#shadows=org.fnives.test.showcase.testutils.shadow.ShadowSnackbar
instrumentedPackages=androidx.loader.content