Firebase emulator testing (#9)

- Firebase emulator added
 - Update to Espresso tests
 - Updated gradle dependencies for espresso
 - Updated config.yml
 - Updated android gradle version
This commit is contained in:
2023-03-17 18:37:20 +00:00
committed by GitHub
parent 88604e6970
commit 7f4060f1c2
53 changed files with 491 additions and 183 deletions

View File

@@ -6,15 +6,15 @@ import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.hamcrest.CoreMatchers.*
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anything
open class BaseTestRobot {
fun fillEditText(resId: Int, text: String): ViewInteraction =
fun fillEditText(resId: Int, text: String?): ViewInteraction =
onView(withId(resId)).perform(ViewActions.replaceText(text), ViewActions.closeSoftKeyboard())
fun clickButton(resId: Int): ViewInteraction = onView((withId(resId))).perform(ViewActions.click())
@@ -22,7 +22,7 @@ open class BaseTestRobot {
fun textView(resId: Int): ViewInteraction = onView(withId(resId))
fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction
.check(ViewAssertions.matches(ViewMatchers.withText(text)))
.check(matches(ViewMatchers.withText(text)))
fun matchText(resId: Int, text: String): ViewInteraction = matchText(textView(resId), text)

View File

@@ -1,38 +1,60 @@
package h_mal.appttude.com
import android.view.View
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.rule.ActivityTestRule
import h_mal.appttude.com.espresso.IdlingResourceClass
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Rule
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.platform.app.InstrumentationRegistry
import h_mal.appttude.com.base.BaseActivity
import org.hamcrest.Matcher
import org.junit.After
import org.junit.Before
abstract class BaseUiTest<T : AppCompatActivity> {
@Ignore
abstract fun getApplicationClass(): Class<T>
open class BaseUiTest<T : BaseActivity<*>>(
private val activity: Class<T>
) {
@get:Rule
var mActivityTestRule = ActivityTestRule(getApplicationClass())
private lateinit var mActivityScenarioRule: ActivityScenario<T>
private var mIdlingResource: IdlingResource? = null
companion object {
@BeforeClass
@JvmStatic
fun setUp() {
IdlingRegistry.getInstance().register(IdlingResourceClass.countingIdlingResource)
}
@AfterClass
@JvmStatic
fun tearDown() {
IdlingRegistry.getInstance().unregister(IdlingResourceClass.countingIdlingResource)
@Before
fun setup() {
beforeLaunch()
mActivityScenarioRule = ActivityScenario.launch(activity)
mActivityScenarioRule.onActivity {
mIdlingResource = it.getIdlingResource()!!
IdlingRegistry.getInstance().register(mIdlingResource)
}
}
fun getResourceString(@StringRes stringRes: Int): String {
return mActivityTestRule.activity.getString(stringRes)
@After
fun tearDown() {
mIdlingResource?.let {
IdlingRegistry.getInstance().unregister(it)
}
}
fun getResourceString(@StringRes stringRes: Int): String {
return InstrumentationRegistry.getInstrumentation().targetContext.resources.getString(
stringRes
)
}
fun waitFor(delay: Long) {
onView(isRoot()).perform(object : ViewAction {
override fun getConstraints(): Matcher<View> = isRoot()
override fun getDescription(): String? = "wait for $delay milliseconds"
override fun perform(uiController: UiController, v: View?) {
uiController.loopMainThreadForAtLeast(delay)
}
})
}
open fun beforeLaunch() {}
}

View File

@@ -0,0 +1,9 @@
package h_mal.appttude.com
private const val apiKey = "test_key"
const val signUpFirebase = "http://identitytoolkit.googleapis.com/v1/accounts:signUp?key=$apiKey"
const val deleteAccountFirebase = "http://10.0.2.2:9099/identitytoolkit.googleapis.com/v1/accounts:delete?key=$apiKey"
const val USER_PASSWORD = "LetMeIn123!"

View File

@@ -0,0 +1,87 @@
package h_mal.appttude.com
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.storage.FirebaseStorage
import h_mal.appttude.com.base.BaseActivity
import h_mal.appttude.com.data.FirebaseAuthSource
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.tasks.await
import org.junit.After
import org.junit.BeforeClass
open class FirebaseTest<T : BaseActivity<*>>(
activity: Class<T>,
private val registered: Boolean = false,
private val signedIn: Boolean = false
) : BaseUiTest<T>(activity) {
private val firebaseAuthSource by lazy { FirebaseAuthSource() }
private var email: String? = null
companion object {
/**
* Setup firebase emulators before all tests
*/
@JvmStatic
@BeforeClass
fun setupFirebase() {
val localHost = "10.0.2.2"
FirebaseAuth.getInstance().useEmulator(localHost, 9099)
FirebaseDatabase.getInstance().useEmulator(localHost, 9000)
FirebaseStorage.getInstance().useEmulator(localHost, 9199)
}
}
override fun beforeLaunch() {
if (registered) {
runBlocking {
setupUser()
}
if (!signedIn) firebaseAuthSource.logOut()
}
}
@After
fun tearDownFirebase() = runBlocking {
removeUser()
firebaseAuthSource.logOut()
}
suspend fun setupUser(
signInEmail: String = generateEmailAddress(),
password: String = USER_PASSWORD
) {
email = signInEmail
firebaseAuthSource.registerUser(signInEmail, password).await().user
}
// remove the user we created for testing
suspend fun removeUser() {
try {
getEmail()?.let {
if (firebaseAuthSource.getUser() == null) firebaseAuthSource.signIn(email = it, password = USER_PASSWORD).await()
firebaseAuthSource.reauthenticate(it, USER_PASSWORD).await()
firebaseAuthSource.deleteProfile().await()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun generateEmailAddress(): String {
val suffix = (1000..50000).random()
email ="test-${suffix}@test-account.com"
return email!!
}
fun getEmail(): String? {
firebaseAuthSource.getUser()?.email?.let {
return it
}
return email
}
}

View File

@@ -0,0 +1,62 @@
package h_mal.appttude.com
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.*
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class WebUtils {
private val okHttpClient by lazy { OkHttpClient() }
suspend fun <T : Any> post(url: String, body: String): T? {
val requestBody = body.toRequestBody()
val request = Request.Builder()
.post(requestBody)
.url(url)
.build()
return okHttpClient.newCall(request).await()
}
suspend fun get(url: String): String? {
val request: Request = Request.Builder()
.url(url)
.build()
return okHttpClient.newCall(request).await()
}
private suspend fun <T> Call.await(): T? {
val objectMapper = Gson()
val typeToken: TypeToken<T> = object : TypeToken<T>() {}
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(
objectMapper.fromJson(
response.body?.string(),
typeToken.type
)
)
}
override fun onFailure(call: Call, e: IOException) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
continuation.resumeWithException(e)
}
})
continuation.invokeOnCancellation {
try {
cancel()
} catch (ex: Throwable) {
//Ignore cancel exception
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
package h_mal.appttude.com.firebase
data class SignUpResponse(
val expiresIn: String? = null,
val kind: String? = null,
val idToken: String? = null,
val localId: String? = null,
val email: String? = null,
val refreshToken: String? = null
)

View File

@@ -1,17 +0,0 @@
package h_mal.appttude.com.robots
import h_mal.appttude.com.BaseTestRobot
import h_mal.appttude.com.R
fun home(func: HomeRobot.() -> Unit) = HomeRobot().apply { func() }
class HomeRobot: BaseTestRobot() {
fun checkTitleExists(title: String) = matchText(R.id.prova_title_tv, title)
fun clickLogin() = clickButton(R.id.email_sign_in_button)
fun clickRegister() = clickButton(R.id.register_button)
fun clickForgotPassword() = clickButton(R.id.forgot)
}

View File

@@ -1,24 +0,0 @@
package h_mal.appttude.com.robots
import h_mal.appttude.com.BaseTestRobot
import h_mal.appttude.com.R
fun login(func: LoginRobot.() -> Unit) = LoginRobot().apply { func() }
class LoginRobot: BaseTestRobot() {
fun setEmail(email: String) = fillEditText(R.id.email, email);
fun setPassword(pass: String) = fillEditText(R.id.password, pass)
fun clickLogin() = clickButton(R.id.email_sign_in_button)
fun clickRegister() = clickButton(R.id.register_button)
fun clickForgotPassword() = clickButton(R.id.forgot)
fun checkEmailError(err: String) = checkErrorOnTextEntry(R.id.email, err)
fun checkPasswordError(err: String) = checkErrorOnTextEntry(R.id.password, err)
}

View File

@@ -1,27 +0,0 @@
package h_mal.appttude.com.robots
import h_mal.appttude.com.BaseTestRobot
import h_mal.appttude.com.R
fun register(func: RegisterRobot.() -> Unit) = RegisterRobot().apply { func() }
class RegisterRobot: BaseTestRobot() {
fun setName(name: String) = fillEditText(R.id.name_register, name)
fun setEmail(email: String) = fillEditText(R.id.email_register, email)
fun setPassword(pass: String) = fillEditText(R.id.password_top, pass)
fun setPasswordConfirm(pass: String) = fillEditText(R.id.password_bottom, pass)
fun clickLogin() = clickButton(R.id.email_sign_up)
fun checkNameError(err: String) = checkErrorOnTextEntry(R.id.name_register, err)
fun checkEmailError(err: String) = checkErrorOnTextEntry(R.id.email_register, err)
fun checkPasswordError(err: String) = checkErrorOnTextEntry(R.id.password_top, err)
fun checkPasswordConfirmError(err: String) = checkErrorOnTextEntry(R.id.password_bottom, err)
}

View File

@@ -1,42 +0,0 @@
package h_mal.appttude.com.tests
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import com.google.firebase.auth.FirebaseAuth
import h_mal.appttude.com.BaseUiTest
import h_mal.appttude.com.R
import h_mal.appttude.com.robots.home
import h_mal.appttude.com.ui.user.LoginActivity
import h_mal.appttude.com.robots.login
import h_mal.appttude.com.robots.register
import org.junit.*
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class LoginActivityTest: BaseUiTest<LoginActivity>() {
@Ignore
override fun getApplicationClass() = LoginActivity::class.java
@After
fun afterTest(){
FirebaseAuth.getInstance().signOut()
}
@Test
fun verifyUserLogin_validUsernameAndPassword_loggedIn() {
login {
setEmail("test-user@testuserdriver.com")
setPassword("Password1234")
clickLogin()
}
home {
checkTitleExists(getResourceString(R.string.welcome_title))
}
}
}