Issue#13 Add CodeKata for Robolectric Tests
This commit is contained in:
parent
c38e608c8c
commit
03e413fba6
25 changed files with 758 additions and 296 deletions
|
|
@ -38,7 +38,7 @@ android {
|
|||
|
||||
sourceSets {
|
||||
androidTest {
|
||||
java.srcDirs += "src/sharedTest/java"
|
||||
// java.srcDirs += "src/sharedTest/java"
|
||||
assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
test {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,6 @@ package org.fnives.test.showcase.testutils.configuration
|
|||
|
||||
object SpecificTestConfigurationsFactory : TestConfigurationsFactory {
|
||||
|
||||
override fun createSnackbarVerification(): SnackbarVerificationHelper =
|
||||
AndroidTestSnackbarVerificationHelper
|
||||
|
||||
override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory =
|
||||
AndroidMigrationTestRuleFactory
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,6 @@ package org.fnives.test.showcase.testutils.configuration
|
|||
|
||||
object SpecificTestConfigurationsFactory : TestConfigurationsFactory {
|
||||
|
||||
override fun createSnackbarVerification(): SnackbarVerificationHelper =
|
||||
RobolectricSnackbarVerificationHelper
|
||||
|
||||
override fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory =
|
||||
RobolectricMigrationTestHelperFactory
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package org.fnives.test.showcase.ui.codekata
|
||||
|
||||
class CodeKataLoginRobot {
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,5 @@ package org.fnives.test.showcase.testutils.configuration
|
|||
*/
|
||||
interface TestConfigurationsFactory {
|
||||
|
||||
fun createSnackbarVerification(): SnackbarVerificationHelper
|
||||
|
||||
fun createSharedMigrationTestRuleFactory(): SharedMigrationTestRuleFactory
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package org.fnives.test.showcase.testutils.robot
|
||||
|
||||
interface Robot {
|
||||
|
||||
fun init()
|
||||
|
||||
fun release()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue