Firebase Authentication With Jetpack Compose. Testing. Part 2
Tags: android, androiddev, testing, mobile
Published: 2024-06-27 15:09:35

Nice to meet you here. This post is the second part of a series of Firebase Authentication with Jetpack Compose. Today we're going to implement UI and Unit testing with the help of Robolectric and MockK. Make sure to have a tab with [this post opened](https://dev.to/evgensuit/firebase-authentication-with-jetpack-compose-part-1-3k82)

---
## What is Robolectric?
Robolectric is a framework which enables testing Android applications without an emulator, directly on a local computer. It does so by emulating Android environment and its components. Robolectric tests are primarily used in UI and Integration testing. Robolectric tests use the same syntax for verifying UI components as regular UI tests run on an emulator.

---
## What is MockK?
MockK is a Unit testing framework. It allows to mock, or fake, our code. Mocking can make certain functions or properties return the result we want.

## Setup

```gradle
dependencies {
    val mockkVersion = "1.13.11"
    val robolectricVersion = "4.12.1"
    testImplementation("io.mockk:mockk-android:$mockkVersion")
    testImplementation("io.mockk:mockk-agent:$mockkVersion")
    testImplementation("org.robolectric:robolectric:$robolectricVersion ")
}
```

Create an `auth` package inside of `test [unitTest]` folder together with files for the code. For me it looks like this

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p30dio9uy03vbx1r9w5i.png)
`Common.kt` file should contain shared authentication mocking function so that we could reuse it in UI and Unit testing

---

Create a `Helpers.kt` file and insert the following code

```kotlin
val userId = "id"
class CorrectAuthData {
    companion object {
        const val USERNAME: String = "Evgen"
        const val EMAIL: String = "someemail@gmail.com"
        const val PASSWORD: String = "SomePassword123"
    }
}
class IncorrectAuthData {
    companion object {
        const val USERNAME: String = " "
        const val EMAIL: String = "incorrect"
        const val PASSWORD: String = " "
    }
}
inline fun <reified T> mockTask(result: T? = null, exception: Exception? = null): Task<T> {
    val task = mockk<Task<T>>()
    every { task.result } returns result
    every { task.exception } returns exception
    every { task.isCanceled } returns false
    every { task.isComplete } returns true
    return task
}
```
`mockTask` function is defined as `reified` so that it could take in any type of Firebase task (T) as a parameter, which is inserted automatically when the `Task` starts executing. E.g, Firestore `set` function is of the following type: `com.google.android.gms.tasks.Task<Void>`, while `signInWithEmailAndPassword` is of `com.google.android.gms.tasks.Task<com.google.firebase.auth.AuthResult>` type

---

Inside of `Common.kt` file define the following function
```kotlin
fun mockAuth(userProfileChangeRequest: CapturingSlot<UserProfileChangeRequest>? = null): FirebaseAuth {
    val user = mockk<FirebaseUser>{
        every { uid } returns userId
        every { displayName } returns CorrectAuthData.USERNAME
        every { updateProfile(if (userProfileChangeRequest != null) capture(userProfileChangeRequest) else any()) } returns mockTask()
    }
    return mockk {
        every { currentUser } returns user
        every { signOut() } answers {
            every { currentUser } returns null
        }
        every { createUserWithEmailAndPassword(any(), any()) } answers {
            every { currentUser } returns user
            mockTask()
        }
        every { signInWithEmailAndPassword(any(), any()) } answers {
            every { currentUser } returns user
            mockTask()
        }
    }
}
```
As already said at the beginning, mocking allows us to make certain functions or properties of a mocked object return the values we want them to return. In the code above we define a function which returns a mocked instance of Firebase auth. The mocking syntax is clear: for `every` call to `currentUser` return a `user` instance of `FirebaseUser`. `answers` keyword enables us to mock other objects on function call.
`userProfileChangeRequest` is inserted as a slot to the `updateProfile` function call. With this we can verify that a call to `updateProfile` was made with a specific instance of `UserProfileChangeRequest`. We could also verify its `displayName` property.

---

After that define a `BaseTestClass` which will hold basic properties and methods of every test class and which every test class will implement.

```kotlin
open class BaseTestClass {
    val testScope = TestScope()
    val snackbarScope = TestScope()
    lateinit var auth: FirebaseAuth
}
```

---

## Unit testing code
Inside of `AuthUnitTests` class define an `init` function which is going to initialize authentication and view model instances.

```kotlin
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class AuthUnitTests: BaseTestClass() {
    private val userProfileChangeRequestSlot = slot<UserProfileChangeRequest>()
    private lateinit var viewModel: AuthViewModel

    @Before
    fun init() {
        auth = mockAuth(userProfileChangeRequestSlot)
        val repository = AuthRepository(auth, firestore)
        viewModel = AuthViewModel(repository, CoroutineScopeProvider(testScope))
    }
}
```
In my [previous article](https://dev.to/evgensuit/firebase-authentication-with-jetpack-compose-part-1-3k82) I've touched on a topic of inserting `CoroutineScope` instances. The code above demonstrates that. 

---

Next we're going to test how username, password, and email validators work together with `AuthViewModel`. First let's define a function inside of our test class which makes sure that validators react appropriately to the input of incorrect format.

```kotlin
@Test
    fun incorrectInput_error() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        val resources = context.resources
        viewModel.apply {
            onUsername("")
            assertEquals(uiState.value.validationState.usernameValidationError.asString(context),
                resources.getString(R.string.username_not_long_enough))
            
            onEmail("dfjdjfk")
            assertEquals(uiState.value.validationState.emailValidationError.asString(context),
                resources.getString(R.string.invalid_email_format))
            
            onPassword("dfdf")
            assertEquals(uiState.value.validationState.passwordValidationError.asString(context),
                resources.getString(R.string.password_not_long_enough))
            onPassword("eirhgejbrj")
            assertEquals(uiState.value.validationState.passwordValidationError.asString(context),
                resources.getString(R.string.password_not_enough_uppercase))
            onPassword("Geirhgejbrj")
            assertEquals(uiState.value.validationState.passwordValidationError.asString(context),
                resources.getString(R.string.password_not_enough_digits))
        }
    }
```
Here we're using dummy input values and assert that a validator's error is equal to the one defined in string resources.

---

Next we're going to test how validators and the view model react to input of correct format

```kotlin
@Test
    fun correctInput_success() {
        viewModel.apply {
            onUsername(CorrectAuthData.USERNAME)
            assertTrue(uiState.value.validationState.usernameValidationError == StringValue.Empty)

            onEmail(CorrectAuthData.EMAIL)
            assertTrue(uiState.value.validationState.emailValidationError == StringValue.Empty)

            onPassword(CorrectAuthData.PASSWORD)
            assertTrue(uiState.value.validationState.passwordValidationError == StringValue.Empty)
        }
    }
```

---

Next goes a function which tests the sign up behaviour

```kotlin
@Test
    fun signUp_success() = testScope.runTest {
        viewModel.apply {
            changeAuthType()
            assertEquals(uiState.value.authType, AuthType.SIGN_UP)
            onUsername(CorrectAuthData.USERNAME)
            onEmail(CorrectAuthData.EMAIL)
            onPassword(CorrectAuthData.PASSWORD)
            onCustomAuth()
        }
        advanceUntilIdle()
        verify { auth.createUserWithEmailAndPassword(CorrectAuthData.EMAIL, CorrectAuthData.PASSWORD) }
        assertEquals(userProfileChangeRequestSlot.captured.displayName, CorrectAuthData.USERNAME)
        verify { auth.currentUser!!.updateProfile(userProfileChangeRequestSlot.captured) }
    }
```
`runTest` function allows us to execute `suspend` functions in the test code. It's important to call `runTest` specifically on the `testScope` instance, since this is the scope in which our view model is going to call auth functions. 
In the function above we're first changing auth type to sign up, then insert credentials and call the auth function. 
`advanceUntilIdle` function makes `suspend` functions return immediately.
In the end we're verifying if `createUserWithEmailAndPassword` and `updateProfile` were called with correct parameters, and assert that `displayName` of `UserProfileChangeRequest` instance equals to the one we inserted.

Sign in testing function looks similarly

```kotlin
@Test
    fun signIn_success() = testScope.runTest {
        viewModel.apply {
            assertEquals(uiState.value.authType, AuthType.SIGN_IN)
            onUsername(CorrectAuthData.USERNAME)
            onEmail(CorrectAuthData.EMAIL)
            onPassword(CorrectAuthData.PASSWORD)
            onCustomAuth()
        }
        advanceUntilIdle()
        verify { auth.signInWithEmailAndPassword(CorrectAuthData.EMAIL, CorrectAuthData.PASSWORD) }
    }
```

---

## UI testing code
In `AuthUITests` class define a setup method which, in addition to creating a view model and auth mock, will also set content

```kotlin
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class AuthUITests: BaseTestClass() {
    @get: Rule
    val composeRule = createComposeRule()
    private lateinit var viewModel: AuthViewModel

    @Before
    fun setup() {
        auth = mockAuth()
        createViewModel()
        composeRule.apply {
            setContentWithSnackbar(snackbarScope) {
                AuthScreen(onSignIn = { }, viewModel = viewModel)
            }
        }
    }
    private fun createViewModel() {
        val repository = AuthRepository(auth, firestore)
        viewModel = AuthViewModel(repository, CoroutineScopeProvider(testScope))
    }
}
```
Head on to `Common.kt` file and define a `setContentWithSnackbar` extension function

```kotlin
@OptIn(ExperimentalMaterial3Api::class)
fun ComposeContentTestRule.setContentWithSnackbar(
    coroutineScope: CoroutineScope,
    content: @Composable () -> Unit) {
    setContent {
        val context = ApplicationProvider.getApplicationContext<Context>()
        val snackbarHostState = remember { SnackbarHostState() }
        val snackbarController = SnackbarController(snackbarHostState, coroutineScope, context)
        CompositionLocalProvider(LocalSnackbarController provides snackbarController) {
            CustomErrorSnackbar(snackbarHostState = snackbarHostState,
                swipeToDismissBoxState = rememberSwipeToDismissBoxState())
            content()
        }
    }
}
```

In the same file define 2 more helper functions

```kotlin
@OptIn(ExperimentalCoroutinesApi::class)
fun ComposeContentTestRule.assertSnackbarIsNotDisplayed(snackbarScope: TestScope) {
    waitForIdle()
    snackbarScope.advanceUntilIdle()
    onNodeWithTag(getString(R.string.error_snackbar)).assertIsNotDisplayed()
}
@OptIn(ExperimentalCoroutinesApi::class)
fun ComposeContentTestRule.assertSnackbarTextEquals(snackbarScope: TestScope, message: String) {
    waitForIdle()
    snackbarScope.advanceUntilIdle()
    onNodeWithTag(getString(R.string.error_snackbar)).assertTextEquals(message)
}
```
Before making assertions, we first have to make sure that all coroutines get executed, e.g `LaunchedEffect` in `AuthScreen`

```kotlin
LaunchedEffect(uiState.authResult) {
        snackbarController.showSnackbar(uiState.authResult)
}
```
And the one in `SnackbarController`

```kotlin
fun showSnackbar(result: CustomResult) {
        if (result is CustomResult.DynamicError || result is CustomResult.ResourceError) {
            coroutineScope.launch {
                snackbarHostState.currentSnackbarData?.dismiss()
                snackbarHostState.showSnackbar(result.error.asString(context))
            }
        }
    }
```
That's exactly what `waitForIdle()` and `snackbarScope.advanceUntilIdle()` do.

---

Next let's write the code that verifies that the UI correctly responds to input

```kotlin
    @Test
    fun signIn_testIncorrectInput() {
        composeRule.apply {
            onNodeWithText(getString(R.string.dont_have_an_account)).assertIsDisplayed()
            onNodeWithTag(getString(R.string.email)).performTextReplacement(
                IncorrectAuthData.EMAIL)
            onNodeWithTag(getString(R.string.password)).performTextReplacement(
                IncorrectAuthData.PASSWORD)
            onNodeWithText(getString(R.string.sign_in)).assertIsNotEnabled()
        }
    }
    @Test
    fun signUp_testIncorrectInput() {
        composeRule.apply {
            onNodeWithText(getString(R.string.go_to_signup)).performClick()
            onNodeWithText(getString(R.string.dont_have_an_account)).assertIsNotDisplayed()

            onNodeWithTag(getString(R.string.username)).performTextReplacement(
                IncorrectAuthData.USERNAME)
            onNodeWithText(getString(R.string.username_not_long_enough)).assertIsDisplayed()
            onNodeWithTag(getString(R.string.email)).performTextReplacement(
                IncorrectAuthData.EMAIL)
            onNodeWithText(getString(R.string.invalid_email_format)).assertIsDisplayed()
            onNodeWithTag(getString(R.string.password)).performTextReplacement(
                IncorrectAuthData.PASSWORD)
            onNodeWithText(getString(R.string.password_not_long_enough)).assertIsDisplayed()

            onNodeWithText(getString(R.string.sign_up)).assertIsNotEnabled()
        }
    }
   @Test
    fun signIn_testCorrectInput() {
        composeRule.apply {
            onNodeWithText(getString(R.string.dont_have_an_account)).assertIsDisplayed()
            onNodeWithTag(getString(R.string.email)).performTextReplacement(
                CorrectAuthData.EMAIL)
            onNodeWithTag(getString(R.string.password)).performTextReplacement(
                CorrectAuthData.PASSWORD)
            onNodeWithText(getString(R.string.sign_in)).assertIsEnabled()
        }
    }
    @Test
    fun signUp_testCorrectInput() {
        composeRule.apply {
            onNodeWithText(getString(R.string.go_to_signup)).performClick()
            onNodeWithText(getString(R.string.dont_have_an_account)).assertIsNotDisplayed()
            onNodeWithTag(getString(R.string.username)).performTextReplacement(
                CorrectAuthData.USERNAME)
            onNodeWithTag(getString(R.string.email)).performTextReplacement(
                CorrectAuthData.EMAIL)
            onNodeWithTag(getString(R.string.password)).performTextReplacement(
                CorrectAuthData.PASSWORD)
            onNodeWithText(getString(R.string.sign_up)).assertIsEnabled()
        }
    }
```

---

Recall that sign up must not be available if username is not provided, while sign in must be available even if username is not null. This is what we'll test next

```kotlin
@Test
    fun signInCorrectInputTest_onGoToSignUpClick_isSignUpDisabled() {
        composeRule.apply {
            onNodeWithTag(getString(R.string.email)).performTextReplacement(
                CorrectAuthData.EMAIL)
            onNodeWithTag(getString(R.string.password)).performTextReplacement(
                CorrectAuthData.PASSWORD)
            onNodeWithText(getString(R.string.go_to_signup)).performClick()
            onNodeWithText(getString(R.string.sign_up)).assertIsNotEnabled()
        }
    }
    @Test
    fun signUpCorrectInputTest_onGoToSignInClick_isSignInEnabled() {
        composeRule.apply {
            onNodeWithText(getString(R.string.go_to_signup)).performClick()

            onNodeWithTag(getString(R.string.username)).performTextReplacement(
                CorrectAuthData.USERNAME)
            onNodeWithTag(getString(R.string.email)).performTextReplacement(
                CorrectAuthData.EMAIL)
            onNodeWithTag(getString(R.string.password)).performTextReplacement(
                CorrectAuthData.PASSWORD)

            onNodeWithText(getString(R.string.sign_up)).assertIsEnabled()
            onNodeWithText(getString(R.string.go_to_signin)).performClick()
            onNodeWithText(getString(R.string.sign_in)).assertIsEnabled()
        }
    }
```

---

And finally, let's test how UI reacts on successful and unsuccessful authentication

```kotlin
@Test
    fun signIn_onSuccess_snackbarNotShown() = testScope.runTest {
        composeRule.apply {
            onNodeWithTag(getString(R.string.email)).performTextReplacement(
                CorrectAuthData.EMAIL)
            onNodeWithTag(getString(R.string.password)).performTextReplacement(
                CorrectAuthData.PASSWORD)
            onNodeWithText(getString(R.string.sign_in)).performClick()

            onNodeWithTag(getString(R.string.email)).assertIsNotEnabled()
            onNodeWithTag(getString(R.string.password)).assertIsNotEnabled()
            onNodeWithText(getString(R.string.go_to_signup)).assertIsNotEnabled()
            onNodeWithText(getString(R.string.sign_in)).assertIsNotEnabled()

            advanceUntilIdle()
            assertSnackbarIsNotDisplayed(snackbarScope)
        }
    }
    @Test
    fun signUp_onSuccess_snackbarNotShown() = testScope.runTest {
        composeRule.apply {
            onNodeWithText(getString(R.string.go_to_signup)).performClick()
            onNodeWithTag(getString(R.string.username)).performTextReplacement(
                CorrectAuthData.USERNAME)
            onNodeWithTag(getString(R.string.email)).performTextReplacement(
                CorrectAuthData.EMAIL)
            onNodeWithTag(getString(R.string.password)).performTextReplacement(
                CorrectAuthData.PASSWORD)
            onNodeWithText(getString(R.string.sign_up)).performClick()

            onNodeWithTag(getString(R.string.username)).assertIsNotEnabled()
            onNodeWithTag(getString(R.string.email)).assertIsNotEnabled()
            onNodeWithTag(getString(R.string.password)).assertIsNotEnabled()
            onNodeWithText(getString(R.string.go_to_signin)).assertIsNotEnabled()
            onNodeWithText(getString(R.string.sign_up)).assertIsNotEnabled()

            advanceUntilIdle()
            assertSnackbarIsNotDisplayed(snackbarScope)
        }
    }
    @Test
    fun signIn_onError_snackbarShown() = testScope.runTest {
        val exception = Exception("exception")
        every { auth.signInWithEmailAndPassword(CorrectAuthData.EMAIL, CorrectAuthData.PASSWORD) } returns mockTask(exception = exception)
        composeRule.apply {
            onNodeWithTag(getString(R.string.email)).performTextReplacement(
                CorrectAuthData.EMAIL)
            onNodeWithTag(getString(R.string.password)).performTextReplacement(
                CorrectAuthData.PASSWORD)
            onNodeWithText(getString(R.string.sign_in)).performClick()

            onNodeWithTag(getString(R.string.email)).assertIsNotEnabled()
            onNodeWithTag(getString(R.string.password)).assertIsNotEnabled()
            onNodeWithText(getString(R.string.go_to_signup)).assertIsNotEnabled()
            onNodeWithText(getString(R.string.sign_in)).assertIsNotEnabled()

            advanceUntilIdle()
            assertSnackbarTextEquals(snackbarScope, exception.message!!)
        }
    }

    @Test
    fun signUp_onError_snackbarShown() = testScope.runTest {
        val exception = Exception("exception")
        every { auth.createUserWithEmailAndPassword(CorrectAuthData.EMAIL, CorrectAuthData.PASSWORD) } returns mockTask(exception = exception)
        composeRule.apply {
            onNodeWithText(getString(R.string.go_to_signup)).performClick()
            onNodeWithTag(getString(R.string.username)).performTextReplacement(
                CorrectAuthData.USERNAME)
            onNodeWithTag(getString(R.string.email)).performTextReplacement(
                CorrectAuthData.EMAIL)
            onNodeWithTag(getString(R.string.password)).performTextReplacement(
                CorrectAuthData.PASSWORD)
            onNodeWithText(getString(R.string.sign_up)).performClick()

            onNodeWithTag(getString(R.string.username)).assertIsNotEnabled()
            onNodeWithTag(getString(R.string.email)).assertIsNotEnabled()
            onNodeWithTag(getString(R.string.password)).assertIsNotEnabled()
            onNodeWithText(getString(R.string.go_to_signin)).assertIsNotEnabled()
            onNodeWithText(getString(R.string.sign_up)).assertIsNotEnabled()

            advanceUntilIdle()
            assertSnackbarTextEquals(snackbarScope, exception.message!!)
        }
    }
```
`onNodeWithTag(getString(R.string.email))` represents email auth field

The code above clearly demonstrates the advantage of inserting `CoroutineScope` into view models. We could use the `viewModelScope` in Robolectric tests, but it would deprive us of a possibility of verifying `InProgress` state since the suspending code would automatically advance. Also, before making assertions on `InProgress` state, make sure that in the view model you don't update the state inside of coroutine body, e.g:
```kotlin
fun onCustomAuth() {
        val authType = _uiState.value.authType
        updateAuthResult(CustomResult.InProgress)
        scope.launch {
            try {
                if (authType == AuthType.SIGN_UP) {
                    authRepository.signUp(_uiState.value.authState)
                }
                authRepository.signIn(_uiState.value.authState)
                updateAuthResult(CustomResult.Success)
            } catch (e: Exception) {
                updateAuthResult(CustomResult.DynamicError(e.toStringIfMessageIsNull()))
            }
        }
    }
```

---

That's it! If you have any suggestions feel free to leave them in the comments. Good luck!