Testing an Authentication Form with Jetpack Compose

This blog post is a preview of the Authentication Form project from Practical Jetpack Compose. Pick up the course and learn how to build 11 other projects 🚀

In the first post from this preview, we learnt how to build an authentication form using Jetpack Compose, along with how to manage the state for this user interface. in this post, we’ll explore how to test this user interface using testing approaches found when it comes to Jetpack Compose.


Now that we’ve built our Authentication screen, we’re going to take a look at how we can write tests for our composables. We’re going to be writing some instrumentation tests using the compose ui-test-junit package – allowing us to verify that our composables are displayed and functioning as expected.

Before we can get started with our tests, we’re going to need to add a couple of test specific dependencies to our project:

androidTestImplementation(
	"androidx.compose.ui:ui-test-junit4:$compose_version")
debugImplementation(
	"androidx.compose.ui:ui-test-manifest:$compose_version")

We’re also going to need to add mocks to our test – this allows us to easily provide mock references to any listeners that are provided to our composable functions, allowing us to easily verify they are triggered whenever expected.

androidTestImplementation(
	"org.mockito.kotlin:mockito-kotlin:3.2.0")
androidTestImplementation("org.mockito:mockito-android:3.12.4")

With these in place, we now have access to the required rules and functionality that allow us to test our composable UI. However, alongside these dependencies, we’re also going to need to add some rules to our build.gradle file that will fix some of the compilation errors that we’d currently see when trying to run our tests. Here we’ll add some packagingOptions that will exclude certain packages from the added dependencies. We won’t dive too much into this concept and it’s usually dependant on the versions of dependencies that are being used, so this may be redundant if you come to updating versions.

android {
	packagingOptions {
		exclude "**/attach_hotspot_windows.dll"
		exclude "META-INF/AL2.0"
		exclude "META-INF/LGPL2.1"
		exclude "META-INF/licenses/ASM"
	}
}

Setting up the test class

We’ll next start by creating a new class, AuthenticationTest – this class will be used to contain the different tests that we’re going to write.

class AuthenticationTest {

}

Inside of this class, we now need to define a reference to the ComposeContentTestRule class – this is what we’re going to use to set the composable content on screen, allowing us to perform interactions and assertions from within our tests.

@get:Rule
val composeTestRule = createComposeRule()

When using this rule, we don’t need to specify any form of activity for our composables to be launched in, the test rule will handle that for us. So using this rule we will set the composable content to be composed, the test will then launch a host activity which will be used to compose our provided content inside of.


Testing the Authentication Composable

At the root of our feature is the Authentication composable. What makes this different from our lower level composables (such as each UI component composable), is that this composable allows us to interact with elements to trigger state updates and recomposition of our UI. This means that with the tests for the Authentication composable we can assert not only that the expected UI components are displayed, but also that interactions with them result in the expected state changes.

Testing for Authentication Mode changes

With that in mind, we’re going to start with some tests to ensure that the expected state is composed when the UI is interacted with. Within the Authentication composable the user can toggle between the sign-in + sign-up mode, so we’ll start with some tests that assert the compositions that are dependant on these different modes.

To start with, the title of the authentication screen is depending on the authentication mode represented within our state. So here, we’re going to write some tests to assert that the content of the title correctly reflects the state of our screen. By default our screen is in the sign in state, so we’re going to write a test to assert that this is the case.

To write this first test we’ll use the @Test annotation and create a new function to test that the Sign In title is displayed by default within our UI.

@Test
fun Sign_In_Title_Displayed_By_Default() {

}

Inside of this test, we’re going to need to start by setting the composable content that is to be displayed on screen for us to assert against. Here we’ll use the test rule that we previously defined, along with its setContent function. This function takes a composable function as an argument, allowing us to define what is to be composed on screen for our tests. Because we’re wanting to test the Authentication Composable that we defined in the previous sections of this chapter, we’ll go ahead and pass the Authentication composable function for this composable argument.

@Test
fun Sign_In_Title_Displayed_By_Default() {
    composeTestRule.setContent {
		Authentication()
	}
}

While we aren’t yet performing any assertions, running this test will launch an activity that displays the content of our Authentication composable. With this now being displayed, we can next perform the required assertions to ensure that the Sign In title is being displayed within our composable UI. We’ll do this by utilising the onNodeWithText function from our test rule reference.

The onNodeWithText function can be used to locate a composable that is displaying the text that we have provided to the function. Composables will be located in the form of a semantic node – because our composables are represented via semantics, in our tests we are going to be locating nodes within our semantic tree. In this case, this is done using the onNodeWithText function, which will return us with a SemanticsNodeInteraction reference to perform assertions against.

@Test
fun Sign_In_Title_Displayed_By_Default() {
	composeTestRule.setContent {
		Authentication()
	}

	composeTestRule.onNodeWithText(
        InstrumentationRegistry.getInstrumentation()
			.context.getString(
                R.string.label_sign_in_to_account)
    )
}

For this test we want to assert that this node is being displayed within our composed UI, so we’re going to go ahead and utilise the assertIsDisplayed function. This is one of the assertions available on the SemanticsNodeInteraction class, allowing us to assert whether this node is being displayed on the screen.

@Test
fun Sign_In_Title_Displayed_By_Default() {
	composeTestRule.setContent {
		Authentication()
	}

	composeTestRule.onNodeWithText(
        InstrumentationRegistry.getInstrumentation()
			.context.getString(
                R.string.label_sign_in_to_account)
    ).assertIsDisplayed()
}

If you run this test within your IDE, you’ll not only see the UI spin up inside of the connected device / emulator, but the tests should also be passing due to the required string being composed within the UI.

Alongside the Sign In title being displayed by the default Sign In authentication mode, the Need Account account should also be composed within our UI. For these tests, we’re going to be able to reuse a lot of the same code, with the key difference here being that we need to assert the composition of the action_need_account string as opposed to the sign in title.

@Test
fun Need_Account_Displayed_By_Default() {
	composeTestRule.setContent {
		Authentication()
	}

	composeTestRule
		.onNodeWithText(
        	InstrumentationRegistry.getInstrumentation()
				.context.getString(
					R.string.action_need_account)
        )
		.assertIsDisplayed()
}

With these two tests in place, we are now able to assert that the expected composables for the Sign In Authentication Mode are being composed. But what happens if the user toggles the Authentication Mode? In this scenario, we know that toggling to the Sign Up mode will change the title and toggle messages – so we’re going to write some tests to assert these recompositions are triggered when this toggle occurs.

For this, we’re going to start by testing that the title is changed to the Sign Up title when the Authentication Mode toggle is clicked. Just like the last test, we’ll start by setting the content to be composed inside of our UI.

@Test
fun Sign_Up_Title_Displayed_After_Toggled() {
    composeTestRule.setContent {
        Authentication()
    }
}

Now that we have some form of state being composed into our UI, we’re going to want to trigger the title change – this will be done by clicking the Authentication Mode toggle button, which switches our screen from the sign-in to sign-up state. For this interaction we’re going to use the performClick function, this is a gesture action that is available on the SemanticsNodeInteraction class, allowing us to perform a click gesture on the specified node.

@Test
fun Sign_Up_Title_Displayed_After_Toggled() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule
		.onNodeWithText(
        	InstrumentationRegistry.getInstrumentation()
				.context.getString(
					R.string.action_need_account)
    	).performClick()
}

Once this interaction has taken place, it is expected that the authentication mode will be toggled. When this occurs, the title of the screen should switch to represent the Sign Up mode. So with this test, we want to assert that the expected Sign Up title is composed. Here we’ll match the assertion that we used for the title in the previous test, except this time we’ll check for our R.string.label_sign_up_for_account String resource.

@Test
fun Sign_Up_Title_Displayed_After_Toggled() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule
		.onNodeWithText(
        	InstrumentationRegistry.getInstrumentation()
				.context.getString(
					R.string.action_need_account)
    	).performClick()

    composeTestRule
		.onNodeWithText(
            InstrumentationRegistry.getInstrumentation()
				.context.getString(
                	R.string.label_sign_up_for_account)
    	).assertIsDisplayed()
}

With this test in place, we’re now able to assert that our title is recomposed accordingly when the authentication mode is toggled. When this toggle occurs, we will also expect the authentication button to be recomposed to reflect the Sign Up action, as opposed to Sign In. For this, we’re going to start with a test that looks very similar to the previous – we’ll need to compose our Authentication composable, followed by using the performClick function to interact with the button used to toggle the authentication mode.

@Test
fun Sign_Up_Button_Displayed_After_Toggle() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule
		.onNodeWithText(
       		InstrumentationRegistry.getInstrumentation()
				.context.getString(
					R.string.action_need_account)
    	).performClick()
}

Now, this is clicked, our authentication button should be showing the Sign Up action. We’re going to want to assert this to ensure this is the case, so we’ll need to start by adding a tag to the authentication button composable. We’ll create a new object, Tags, and define a new tag that can be assigned to our authentication button. We’re also going to be interacting with the authentication toggle across a couple of tests, so we’ll add a tag for that too.

//Tags.kt

object Tags {
    const val TAG_AUTHENTICATE_BUTTON = "authenticate_button"
	const val TAG_AUTHENTICATION_TOGGLE = "authentication_mode_toggle"
}

With these tags defined we can now use the testTag function to assign this tag to our Button composable.

//AuthenticationButton.kt

Button(
	modifier = Modifier.testTag(TAG_AUTHENTICATE_BUTTON),
	...
)

We’ll also do the same for the ToggleAuthenticationMode Button.

// ToggleAuthenticationMode.kt

TextButton(
	modifier = Modifier.testTag(TAG_AUTHENTICATION_TOGGLE),
	...
)

With these tags in place, it can now be used to locate a node within our composable hierarchy using the onNodeWithTag function. On this node, we can then use the assertTextEquals function to assert that the text of this composable is equal to the content of the action_sign_up resource.

@Test
fun Sign_Up_Button_Displayed_After_Toggle() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule.onNodeWithTag(
       	TAG_AUTHENTICATION_TOGGLE
    ).performClick()

    composeTestRule.onNodeWithTag(
        TAG_AUTHENTICATE_BUTTON
    ).assertTextEquals(
       	InstrumentationRegistry.getInstrumentation()
			.context.getString(R.string.action_sign_up)
    )
}

With this test in place, we can now be certain that when the authentication mode is toggled, the content displayed within the authentication button no longer represents the sign in action, but instead the sign up action.

Aside from the title, we’re going to want to check that our authentication toggle now displays the content that reflects the change authentication mode. For this, we’re going to again interact with the toggle button to toggle the authentication mode, and then we’ll want to assert that the text of that authentication button represents the expected value.

Here we’ll set up a test that will compose our Authentication composable, followed by utilising our TAG_AUTHENTICATION_TOGGLE tag to again locate the node that represents the toggle button. We can then use this reference to perform actions and assertions.

Because we’re going to be performing multiple interactions on this node, we’ll use the kotlin apply function to chain multiple operations. We’ll start by using the performClick function to perform a click action on the button, when this is clicked the authentication mode will be flipped from sign in to sign up. When this occurs, the button should display the content of our action_already_have_account resource. We’ll use the assertTextEquals function to assert that this is the case.

@Test
fun Already_Have_Account_Displayed_After_Toggle() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule.onNodeWithTag(
        TAG_AUTHENTICATION_TOGGLE
    ).apply {
        performClick()
        assertTextEquals(
            InstrumentationRegistry.getInstrumentation()
				.context.getString(
					R.string.action_already_have_account)
        )
    }
}

If this test passes, it means that the authentication toggle button is being successfully recomposed with the corresponding state for the sign-up mode that has been switched to. Otherwise, it means the composable has not been composed with the expected state.

The title and authentication button are dynamic components in the sense that any composition should take into account the authentication mode. Now we have these tests, we’re able to assert that the content of these composables correctly reflects the current authentication mode within our state.

Testing the Authentication Button

Aside from adapting to the authentication mode, the Authentication button is also composed based on other parts of our state. Based on the current content that is input into the email and password text fields, the authentication button will be composed with an enabled state. This means that if the email or password in our state is empty, then the authentication button will be disabled.

By default, our authentication button should be disabled, as there will be no content in either of the email or password properties of our state. Similar to the previous tests we’ve written for our Authentication composable, we’re going to create a new test that composes our Authentication composable, followed by using the test rule to locate a node using our previously defined TAG_AUTHENTICATE_BUTTON tag.

@Test
fun Authentication_Button_Disabled_By_Default() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule
		.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
}

If this node has been located, then we’re going to need to perform an assertion to check that the button is disabled – this is because there the email and password state properties are currently empty. To perform this assertion we’re going to use the assertIsNotEnabled function. This SemanticsMatcher will check that the semantics for the location node has the SemanticsProperties.Disabled property, meaning that the composable is disabled.

@Test
fun Authentication_Button_Disabled_By_Default() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule
		.onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
		.assertIsNotEnabled()
}

With this small test, we’ll now be able to assert that by default, the authentication button is disabled. On the flip side, when those input fields do have content, we want to assert that the Authentication button is enabled. We’ll start writing a new test here to assert this condition.

@Test
fun Authentication_Button_Enabled_With_Valid_Content() {
    composeTestRule.setContent {
        Authentication()
    }
}

While we could use the Authentication state to preload values to be used for the email and password fields, I wanted to simulate user behaviour here – so we’re going to use the performTextInput function to type some text into the specified text field. Before we can interact with our text fields in such a way, we’re going to need to add tags for them so that the nodes can be located from our UI.

const val TAG_INPUT_EMAIL = "input_email"
const val TAG_INPUT_PASSWORD = "input_password"

We’ll then assign these tags to each of the email and password input fields using the testTag modifier.

// EmailInput.kt

TextField(
    modifier = modifier.testTag(TAG_INPUT_EMAIL),
    ...
)

// PasswordInput.kt

TextField(
    modifier = modifier.testTag(TAG_INPUT_PASSWORD),
    ...
)

With these tags in place, we can now utilise the performTextInput function to input a provided string into the corresponding nodes.

composeTestRule.onNodeWithTag(
    TAG_INPUT_EMAIL
).performTextInput("contact@compose.academy")

composeTestRule.onNodeWithTag(
    TAG_INPUT_PASSWORD
).performTextInput("password")

After our state has been composed, we’ll use both the email and password input fields to perform text input – giving both of these fields valid content that would allow the user to authenticate against. Once these calls are in place, we can again locate the Authentication Button using its tag but this time assert that it is enabled using the assertIsEnabled function. We previously used the assertIsNotEnabled function, the key difference here is that assertIsEnabled is checking that the SemanticsProperties.Disabled semantic property is not present on the specific composable.

@Test
fun Authentication_Button_Enabled_With_Valid_Content() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule.onNodeWithTag(
        TAG_INPUT_EMAIL
    ).performTextInput("contact@compose.academy")

    composeTestRule.onNodeWithTag(
        TAG_INPUT_PASSWORD
    ).performTextInput("password")

    composeTestRule.onNodeWithTag(
        TAG_AUTHENTICATE_BUTTON
    ).assertIsEnabled()
}

Because the email and password fields have valid content, the authentication button, in this case, should be enabled – which our test should now be asserting for us.

Some further testing here could include removing text from the input fields and asserting that our authentication button is disabled from recomposition. At this point, the initial test for the disabled state, followed by the enable state serves as a minimal requirement for our testing – but feel free to explore further coverage here!

Testing authentication errors

Now that we’ve performed assertions on the content that is used to authenticate the user, we can look at the next stages in the application flow. While the user might be successfully authenticated and move to the next screen, that isn’t always going to be the case – to cover these scenarios, we added a dialog composable to be displayed when an error occurs during authentication.

To assert that this dialog is displayed in the correct scenarios, we’re going to go ahead and start by testing that the dialog is not displayed by default. This will allow us to ensure that users are not going to be shown the error dialog when an error hasn’t happened.

@Test
fun Error_Alert_Not_Displayed_By_Default() {

}

So that we can try to locate the node that represents our alert dialog, we’re going to define another tag.

const val TAG_ERROR_ALERT = "error_alert"

We’ll then assign this tag to our AlertDialog composable using the testTag modifier.

AlertDialog(
	modifier = Modifier.testTag(TAG_ERROR_ALERT),
	...
)

With this tag in place, we can now attempt to locate the node and then perform assertions against it. To do this we’ll use the onNodeWithTag function, followed by using assertDoesNotExist to assert that a node with this tag does not exist – meaning that the error dialog does not currently exist within our UI.

@Test
fun Error_Alert_Not_Displayed_By_Default() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule.onNodeWithTag(
        TAG_ERROR_ALERT
    ).assertDoesNotExist()
}

Now that we know our alert dialog is not showing when an error doesn’t exist, we’re going to want to test the flip side of this and assert that the error dialog is displayed when an error has occurred. We’ll start here by defining a new test function to represent this test case.

@Test
fun Error_Alert_Displayed_After_Error() {

}

Next, we need to compose our state so that the error dialog is displayed – we’ll do this by composing our AuthenticationContent and providing an AuthenticationState reference that has an error value assigned to it.

composeTestRule.setContent {
	AuthenticationContent(
        AuthenticationState(
            error = "Some error"
        )
    ) { }
}

Because our state now has an error value, an alert dialog will be composed within our UI. However, we’re going to want to finalise our test and assert that this is the case. We’ll wrap up this test by locating the alert dialog using the tag we previously assigned to the AlertDialog composable, followed by using the assertIsDisplayed function to verify that the alert dialog has been composed within our UI.

@Test
fun Error_Alert_Displayed_After_Error() {
    composeTestRule.setContent {
        AuthenticationContent(
            AuthenticationState(
                error = "Some error"
            )
        ) { }
    }

    composeTestRule.onNodeWithTag(
        TAG_ERROR_ALERT
    ).assertIsDisplayed()
}

Testing the loading state

When the Authentication Button is clicked, the authentication process is triggered – in this scenario we would likely be making a network request, displaying a progress dialog on-screen in the process. During these state changes, we show and hide a large amount of the UI components, so we want to be sure that these state changes result in the expected UI conditions. We’ll write a couple more tests to ensure that these changes behave as expected.

So that we can perform assertions against the progress indicator, we’ll define a new tag and assign it to our CircularProgressIndicator composable using the testTag modifier.

// Tags.kt

object Tags {
	const val TAG_PROGRESS = "progress"
}

CircularProgressIndicator(
	modifier = Modifier.testTag(TAG_PROGRESS)
)

We’ll then go ahead and add a simple first test that asserts our progress indicator is not composed of the default state of our UI. Here we use the onNodeWithTag function to locate our node using the specified tag, followed by asserting that the node does not exist using the assertDoesNotExist function.

@Test
fun Progress_Not_Displayed_By_Default() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule.onNodeWithTag(
        TAG_PROGRESS
    ).assertDoesNotExist()
}

Now we’ve asserted that our progress indicator is not composed with the default authentication state, we can now write a test to ensure that the loading state is reflected in our composed UI. For this we’ll go ahead and compose our AuthenticationContent, providing a reference to the AuthenticationState class with the isLoading flag marked as true.

With this in place, we can continue to locate the node that represents our loading indicator, followed by performing the assertion that it is displayed within our composed UI.

@Test
fun Progress_Displayed_While_Loading() {
    composeTestRule.setContent {
        AuthenticationContent(
            AuthenticationState(isLoading = true)
        ) { }
    }

    composeTestRule.onNodeWithTag(
        TAG_PROGRESS
    ).assertIsDisplayed()
}

After our operation has finished loading, we’ve hardcoded our ViewModel to set an error state. When this happens, our UI should hide the progress indicator and display the authentication form to the user. For us to assert that this is the case, we’ll need to trigger the authentication process from our UI. To save us entering text into the textfields at runtime, we’ll compose our test UI with some pre-loaded state for the email address and password values.

Once that’s done, we next need to perform a click interaction on our Authentication Button – this will trigger the authentication process and set the error state from our ViewModel. When this happens, our progress indicator should no longer exist in our UI. To ensure that this is the case, we’ll add an assertion by using the assertDoesNotExist function to check that the progress indicator does not exist within our UI.

@Test
fun Progress_Not_Displayed_After_Loading() {
    composeTestRule.setContent {
        AuthenticationContent(
            authenticationState = AuthenticationState(
                email = "contact@compose.academy",
                password = "password"
            )
        ) { }
    }

    composeTestRule.onNodeWithTag(
        TAG_AUTHENTICATE_BUTTON
    ).performClick()

    composeTestRule.onNodeWithTag(
        TAG_PROGRESS
    ).assertDoesNotExist()
}

Once we reach this state of our progress indicator not being displayed (because the loading process has been completed), we’re going to want to ensure that the content of our UI has been composed again – this is the authentication form, allowing users to attempt re-authentication. If this didn’t display again within the UI, things would be quite broken for the users – so we’ll write a test to assert that this is the case.

@Test
fun Content_Displayed_After_Loading()

Before we can perform assertions against the content area of our UI, we’re going to need to define a new tag and assign it to the parent of our content area.

// Tags.kt

object Tags {
	const val TAG_CONTENT = "content"
}

We’ll then need to set this tag on the corresponding composable within our AuthenticationForm.

// AuthenticationForm.kt

Column(
	modifier = Modifier.testTag(TAG_CONTENT),
	horizontalAlignment = Alignment.CenterHorizontally
)

While we could perform assertions against the individual children that already have tags assigned to them, this approach allows us to refer to the content area as a whole. Similar to the previous test, we can perform the authentication flow, followed by performing the assertion that the content area exists within our UI.

Because there has been an error state loaded at this point, there will be an alert dialog composed within our UI. For this reason, we use the exists check instead of a displayed check, this is because the alert dialog will be covered a good chunk of the content UI so we cannot always guarantee that the displayed assertion would be satisfied.

@Test
fun Content_Displayed_After_Loading() {
    composeTestRule.setContent {
        Authentication()
    }

    composeTestRule.onNodeWithTag(
        TAG_INPUT_EMAIL
    ).performTextInput("contact@compose.academy")

    composeTestRule.onNodeWithTag(
        TAG_INPUT_PASSWORD
    ).performTextInput("password")

    composeTestRule.onNodeWithTag(
        TAG_AUTHENTICATE_BUTTON
    ).performClick()

    composeTestRule.onNodeWithTag(
        TAG_CONTENT
    ).assertExists()
}

Testing the Authentication Title

Now that we have tests in place that performs assertions against our Authentication composable, we’re going to focus on writing some fine-grained tests for the individual composable functions that represent our individual setting items. This allows us to focus on performing assertions on the behaviour of the composable function itself, without the concern of our global state. We’ll start here by creating a new test class, AuthenticationTitleTest, configuring our ComposeContentTestRule ready for use.

class AuthenticationTitleTest {

	@get:Rule
    val composeTestRule = createComposeRule()

}

We’re going to start here by writing a test to assert that the composable correctly displays the title corresponding title for the AuthenticationMode that is provided to it. The AuthenticationTitle contains the logic that depicts which string resource is used based on the AuthenticationMode that is provided to it. For this reason, we’ll want to write these tests to ensure this logic is working as expected.

Here we’ll create a new test function, Sign_In_Title_Displayed, where we will assert that the Sign In title is composed when the AuthenticationMode.SIGN_IN is provided for the authenticationMode argument. We’ll start here by composing an AuthenticationTitle, passing the Sign In mode for the authenticationMode.

@Test
fun Sign_In_Title_Displayed() {
    composeTestRule.setContent {
        AuthenticationTitle(
            authenticationMode = AuthenticationMode.SIGN_IN
        )
    }
}

When the AuthenticationTitle is composed for the SIGN_IN AuthenticationMode, it is expected that the label_sign_in_to_account will be displayed. We’ll need to perform an assertion for this in our tests, so we’ll go ahead and use the onNodeWithText function on our test rule to locate a node that has the text contained within our label_sign_in_to_account resource. We’ll then use the assertIsDisplayed function to perform the assertion that this composable is displayed.

@Test
fun Sign_In_Title_Displayed() {
    composeTestRule.setContent {
        AuthenticationTitle(
            authenticationMode = AuthenticationMode.SIGN_IN
        )
    }
    composeTestRule
        .onNodeWithText(
            InstrumentationRegistry.getInstrumentation()
				.context.getString(
                	R.string.label_sign_in_to_account)
        )
        .assertIsDisplayed()
}

If this test fails, it means that the expected title is not being composed for the SIGN_IN AuthenticationMode. On the flip side, we’ll also want to assert that the expected text content is composed for the SIGN_UP AuthenticationMode. This test is going to look the same as the previous, except this time we’ll pass in AuthenticationMode.SIGN_UP when composing the AuthenticationTitle, as well as using the label_sign_up_for_account resource when performing our assertion.

@Test
fun Sign_Up_Title_Displayed() {
    composeTestRule.setContent {
        AuthenticationTitle(
            authenticationMode = AuthenticationMode.SIGN_UP
        )
    }
    composeTestRule
        .onNodeWithText(
            InstrumentationRegistry.getInstrumentation()
				.context.getString(
					R.string.label_sign_up_for_account)
        )
        .assertIsDisplayed()
}

With these tests in place, we can now be sure that the AuthenticationTitle is using the expected string resource during composition, based on the AuthenticationMode that is provided to it.


Testing the Authentication Button

Alongside the AuthenticationTitle being composed based on the AuthenticationMode that is provided to it, the AuthenticationButton also behaves in the same way – we’ll also want to write some tests to assert this composition also. We’ll start by creating a new AuthenticationButtonTest class, setting up the compose rule in the process.

class AuthenticationButtonTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    
}

Next, we’ll write the first test within this test class, which will be used to assert that the Sign In action is displayed within the button when expected. For this we’ll need to compose the AuthenticationButton, passing the AuthenticationMode.SIGN_IN value for the authenticationMode argument.

@Test
fun Sign_In_Action_Displayed() {
    composeTestRule.setContent {
        AuthenticationButton(
            enableAuthentication = true,
            authenticationMode = AuthenticationMode.SIGN_IN,
            onAuthenticate = { }
        )
    }
}

Now that the AuthenticationButton is going to be composed in our test, we can perform the required assertions. We already have the TAG_AUTHENTICATE_BUTTON tag assigned to our composable from some previous tests that we wrote, so we can use this to locate the required node. Once we’ve done that, the assertTextEquals can then be used to assert that the expected text of the retrieved node matches the value that we provide. Here we’ll provide the string value for our action_sign_in resource, which represents the “Sign In” value that is expected to be displayed when the AuthenticationMode.SIGN_IN is provided to the composable.

@Test
fun Sign_In_Action_Displayed() {
    composeTestRule.setContent {
        AuthenticationButton(
            enableAuthentication = true,
            authenticationMode = AuthenticationMode.SIGN_IN,
            onAuthenticate = { }
        )
    }
    composeTestRule
        .onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
        .assertTextEquals(
            InstrumentationRegistry.getInstrumentation()
				.context.getString(R.string.action_sign_in)
        )
}

We’ll also want to assert that the action_sign_up string is displayed when the AuthenticationMode.SIGN_UP is passed to the composable. We’ll write a corresponding test here which will mostly match the previous test we wrote, except we’ll pass AuthenticationMode.SIGN_UP for the authenticationMode argument, along with using the action_sign_up string resource when performing the assertTextEquals assertion.

@Test
fun Sign_Up_Action_Displayed() {
    composeTestRule.setContent {
        AuthenticationButton(
            enableAuthentication = true,
            authenticationMode = AuthenticationMode.SIGN_UP,
            onAuthenticate = { }
        )
    }
    composeTestRule
        .onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
        .assertTextEquals(
            InstrumentationRegistry.getInstrumentation()
				.context.getString(
                	R.string.action_sign_up)
        )
}

Alongside the AuthenticationMode based assertions that we’ve performed above, the AuthenticationButton also takes an onAuthenticate argument. When this lambda is invoked by our AuthenticationButton, the parent composable will use this to trigger the authentication mode – if this broke, users would not be able to perform authentication within our app. For this reason, we’re going to write a test to assert that the lambda is invoked when expected. Here we’re going to pass in a mock lambda function for the onAuthenticate argument. This means that we can use this mock to verify that interactions have taken place based off of composable events.

@Test
fun Authenticate_Triggered() {
    val onAuthenticate: () -> Unit = mock()
    composeTestRule.setContent {
        AuthenticationButton(
            enableAuthentication = false,
            authenticationMode = AuthenticationMode.SIGN_UP,
            onAuthenticate = onAuthenticate
        )
    }
}

With the AuthenticationButton being composed, we’ll now be able to retrieve the node that represents this authentication button using the TAG_AUTHENTICATE_BUTTON tag. We’ll then use the performClick function to perform a click action on this node. When this click action is triggered, this is the point that we would expect the onAuthenticate to be invoked so that the parent composable can handle the authentication event. We can verify this within our test by using mockito and its verify function to assert that the lambda has been invoked. If this is the case, the test will succeed – otherwise, the lambda not being triggered will mean that our verification will not be satisfied and the test will fail.

@Test
fun Authenticate_Triggered() {
    val onAuthenticate: () -> Unit = mock()
    composeTestRule.setContent {
        AuthenticationButton(
            enableAuthentication = false,
            authenticationMode = AuthenticationMode.SIGN_UP,
            onAuthenticate = onAuthenticate
        )
    }

	composeTestRule
        .onNodeWithTag(TAG_AUTHENTICATE_BUTTON)
        .performClick()

    verify(onAuthenticate).invoke()
}

The AuthenticationButton composable also takes an enableAuthenticationargument. It could also be beneficial to write some tests to assert the composition based on the value of this argument – we already have some tests for the Authentication composable that involved the enabled state, so we won’t cover that here!


Testing the Authentication Mode Toggle

So far we’ve been writing tests for various composables that utilise the AuthenticationMode from our state object. The button that is used to toggle this value also takes an AuthenticationMode reference, this is also so that it can be composed to display the corresponding content for the provided AuthenticationMode. After setting up a test class with a corresponding test rule, we’ll create a test function that will be used to assert the action_need_account resource text is displayed within our composable.

class AuthenticationModeToggleTest {

    @get:Rule
    val composeTestRule = createComposeRule()

	@Test
	fun Need_Account_Action_Displayed() {

	}
    
}

Within this test we’ll need to start by composing a ToggleAuthenticationMode, providing the authenticationMode argument in the form of the AuthenticationMode.SIGN_IN value. When this SIGN_IN value is provided, we expect that the corresponding content is displayed inside the button – this is in the form of the action_need_account resource. After locating the node for this composable using the onNodeWithTag(TAG_AUTHENTICATION_TOGGLE) function call, we can assert that this expected text is displayed via the use of the assertTextEquals function.

@Test
fun Need_Account_Action_Displayed() {
    composeTestRule.setContent {
        ToggleAuthenticationMode(
            authenticationMode = AuthenticationMode.SIGN_IN,
            toggleAuthentication = { }
        )
    }
    composeTestRule
		.onNodeWithTag(TAG_AUTHENTICATION_TOGGLE)
        .assertTextEquals(
            InstrumentationRegistry.getInstrumentation()
				.context.getString(
					R.string.action_need_account)
        )
}

We’ll next flip this around so that we can assert that the expected action_already_have_account value is displayed when the AuthenticationMode.SIGN_UP value is provided for the authenticationMode argument. Our test here is going to look the same as above, aside from the tweak to the authenticationMode argument that we pass, along with the action_already_have_account value that is now being provided to the assertTextEquals function call.

@Test
fun Already_Have_Account_Action_Displayed() {
    composeTestRule.setContent {
        ToggleAuthenticationMode(
            authenticationMode = AuthenticationMode.SIGN_UP,
            toggleAuthentication = { }
        )
    }
    composeTestRule
        .onNodeWithTag(TAG_AUTHENTICATION_TOGGLE)
        .assertTextEquals(
            InstrumentationRegistry.getInstrumentation()
				.context.getString(
                	R.string.action_already_have_account)
        )
}

With this test in place, we can now be certain that the passing tests mean the provided authenticationMode value is going to display the expected text inside of our composable. Whenever the user clicks the button that is displaying this text, the lambda function that is provided to the composable should be triggered – this is the toggleAuthentication lambda. If this for some reason was not being triggered, the user would not be able to switch to the sign-up mode – so if a user does not currently have an account, they wouldn’t be able to create one. To ensure this remains functional, let’s write a quick test to assert that this event does occur.

Within this next test, we’re going to pass in a mock lambda function for the toggleAuthentication argument. This means that we can use this mock to verify that interactions have taken place based off of composable events.

@Test
fun Toggle_Authentication_Triggered() {
    val toggleAuthentication: () -> Unit = mock()
    composeTestRule.setContent {
        ToggleAuthenticationMode(
            authenticationMode = AuthenticationMode.SIGN_UP,
            toggleAuthentication = toggleAuthentication
        )
    }
}

With the ToggleAuthenticationMode being composed, we’ll now be able to use the retrieve the node that represents this toggle button using the TAG_AUTHENTICATION_TOGGLE tag. We’ll then use the performClick function to perform a click action on this node. When this click action is triggered, this is the point that we would expect the toggleAuthentication to be invoked so that the parent composable can handle the authentication event. We can verify this within our test by using mockito and its verify function to assert that the lambda has been invoked. If this is the case, the test will succeed – otherwise, the lambda not being triggered will mean that our verification will not be satisfied and the test will fail.

@Test
fun Toggle_Authentication_Triggered() {
    val toggleAuthentication: () -> Unit = mock()
    composeTestRule.setContent {
        ToggleAuthenticationMode(
            authenticationMode = AuthenticationMode.SIGN_UP,
            toggleAuthentication = toggleAuthentication
        )
    }
    composeTestRule
        .onNodeWithTag(TAG_AUTHENTICATION_TOGGLE)
        .performClick()

    verify(toggleAuthentication).invoke()
}

Testing the Email Address Input

When it comes to the EmailInput composable, an email argument is used to provide the content that is to be displayed inside of the text field. This is a very important part of the authentication flow, so we’ll want to write a test to ensure that this provided value is displayed inside of our composable. To do this we’ll need to start by setting up a new test class, EmailInputTest.

class EmailInputTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    
}

We’ll start here by creating a new test, composing the EmailInput composable. We’ll provide empty implementations for the onEmailChanged and onFocusRequested arguments, but will need to provide a string value for the email argument. We’ll create a string variable reference here, providing this to our EmailInput composable.

@Test
fun Email_Displayed() {
    val email = "contact@compose.academy"
    composeTestRule.setContent {
        EmailInput(
            email = email,
            onEmailChanged = { },
            onNextClicked = { }
        )
    }
}

Next, we’ll need to assert that this email value is displayed inside of our composable. In a previous test, we defined the TAG_INPUT_EMAIL tag, so we’ll use this here to locate the node that represents our email text field. Once this node has been located we can utilise the assertTextEquals function to assert that the text semantic value of the node matches our provided email variable.

@Test
fun Email_Displayed() {
    val email = "contact@compose.academy"
    composeTestRule.setContent {
        EmailInput(
            email = email,
            onEmailChanged = {  },
            onNextClicked = { }
        )
    }
    composeTestRule
        .onNodeWithTag(TAG_INPUT_EMAIL)
        .assertTextEquals(email)
}

With this test in place, we can now be certain that a passing test means the provided email value is going to be displayed inside of our composable. When the user enters content into the text field to update this email value that is coming from our state, the lambda function that is provided to the composable is triggered – this is the onEmailChanged lambda. If this for some reason was not being triggered, the user would be unable to enter their email address into the text field. To ensure this remains function, let’s write a quick test to assert that this event does occur.

As before, we’ll need to compose the EmailInput composable to perform our assertions against. We’ll need to provide a string value for the email argument – we’ll create a variable reference for this so that we can assert the lambda is triggered with the expected value. We’re also going to pass in a mock lambda function for the onEmailChanged argument. This means that we can use this mock to verify that interactions have taken place based off of composable events.

@Test
fun Email_Changed_Triggered() {
    val onEmailChanged: (email: String) -> Unit = mock()
    val email = "contact@compose.academy"
    composeTestRule.setContent {
        EmailInput(
            email = email,
            onEmailChanged = onEmailChanged,
            onNextClicked = { }
        )
    }
}

While we have provided this lambda to our composable, we now need to trigger it so that we can verify the expected behaviour. To trigger this lambda we need to enter some text into the input field, which we can do so using the performTextInput on a specified node. We’re going to append some text onto the existing input, which we’ll store in a variable reference, appendedText so that we can use this during the assertion. Here we’ll locate the input field node using our existing tag, followed by inputting this content using the performTextInput function.

val appendedText = ".jetpack"
composeTestRule
	.onNodeWithTag(TAG_INPUT_EMAIL)
	.performTextInput(appendedText)

With this in place, we can now add the check to verify that the lambda function is called as expected. When this is triggered, we would expect that the email value returned here would represent the existing content with the addition of the appendedText value. We can verify this within our test by using mockito and its verify function to assert that the lambda has been invoked with the existing value in the input field (email) appended with the value of appendedText. If this is the case, the test will succeed – otherwise, the lambda not being triggered will mean that our verification will not be satisfied and the test will fail.

@Test
fun Email_Changed_Triggered() {
    val onEmailChanged: (email: String) -> Unit = mock()
    val email = "contact@compose.academy"
    composeTestRule.setContent {
        EmailInput(
            email = email,
            onEmailChanged = onEmailChanged,
            onNextClicked = { }
        )
    }
    val appendedText = ".jetpack"
    composeTestRule
        .onNodeWithTag(TAG_INPUT_EMAIL)
        .performTextInput(appendedText)

    verify(onEmailChanged).invoke(email + appendedText)
}

Testing the Password Input

When it comes to the PasswordInput composable, a password argument is used to provide the content that is to be displayed inside of the text field. This is a very important part of the authentication flow, so we’ll want to write a test to ensure that this provided value is displayed inside of our composable. To do this we’ll need to start by setting up a new test class, PasswordInputTest.

class PasswordInputTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    
}

We’ll start here by creating a new test, composing the PasswordInput composable. We’ll provide empty implementations for the onPasswordChanged and onDoneClicked arguments, but will need to provide a string value for the password argument. We’ll create a string variable reference here, providing this to our PasswordInput composable.

@Test
fun Password_Displayed() {
    val password = "password123"
    composeTestRule.setContent {
        PasswordInput(
            password = password,
            onPasswordChanged = {  },
            onDoneClicked = { }
        )
    }
}

Next, we’ll need to assert that this password value is in fact displayed inside of our composable. In a previous test, we defined the TAG_INPUT_PASSWORD tag, so we’ll use this here to locate the node that represents our password text field. Once this node has been located we can utilise the assertTextEquals function to assert that the text semantic value of the node matches our provided password variable.

@Test
fun Password_Displayed() {
    val password = "password123"
    composeTestRule.setContent {
        PasswordInput(
            password = password,
            onPasswordChanged = {  },
            onDoneClicked = { }
        )
    }
    composeTestRule
        .onNodeWithTag(TAG_INPUT_PASSWORD)
        .assertTextEquals(password)
}

With this test in place, we can now be certain that a passing test means the provided password value is going to be displayed inside of our composable. When the user enters content into the text field to update this password value that is coming from our state, the lambda function that is provided to the composable is triggered – this is the onPasswordChanged lambda. If this for some reason was not being triggered, the user would be unable to enter their password into the text field. To ensure this remains function, let’s write a quick test to assert that this event does occur.

As before, we’ll need to compose the PasswordInput composable to perform our assertions against. We’ll need to provide a string value for the password argument – we’ll create a variable reference for this so that we can assert the lambda is triggered with the expected value. We’re also going to pass in a mock lambda function for the onPasswordChanged argument. This means that we can use this mock to verify that interactions have taken place based off of composable events.

@Test
fun Password_Changed_Triggered() {
    val onEmailChanged: (email: String) -> Unit = mock()
    val password = "password123"
    composeTestRule.setContent {
        PasswordInput(
            password = password,
            onPasswordChanged = {  },
            onDoneClicked = { }
        )
    }
}

While we have provided this lambda to our composable, we now need to trigger it so that we can verify the expected behaviour. To trigger this lambda we need to enter some text into the input field, which we can do so using the performTextInput on a specified node. We’re going to append some text onto the existing input, which we’ll store in a variable reference, appendedText so that we can use this during the assertion. Here we’ll locate the input field node using our existing tag, followed by inputting this content using the performTextInput function.

val passwordText = "456"
composeTestRule
    .onNodeWithTag(TAG_INPUT_PASSWORD)
    .performTextInput(passwordText)

With this in place, we can now add the check to verify that the lambda function is called as expected. When this is triggered, we would expect that the password value returned here would represent the existing content with the addition of the appendedText value. We can verify this within our test by using mockito and its verify function to assert that the lambda has been invoked with the existing value in the input field (password) appended with the value of appendedText. If this is the case, the test will succeed – otherwise, the lambda not being triggered will mean that our verification will not be satisfied and the test will fail.

@Test
fun Password_Changed_Triggered() {
    val onEmailChanged: (email: String) -> Unit = mock()
    val password = "password123"
    composeTestRule.setContent {
        PasswordInput(
            password = password,
            onPasswordChanged = {  },
            onDoneClicked = { }
        )
    }
    val passwordText = "456"
    composeTestRule
        .onNodeWithTag(TAG_INPUT_PASSWORD)
        .performTextInput(passwordText)

    verify(onEmailChanged).invoke(password + passwordText)
}

When it comes to the PasswordInput composable, we implemented the ability to toggle the visibility of the password using a visibility toggle button. We’re going to write a test to assert that the state of this is reflected correctly, based on the internal state of the function that is being used to manage the password visibility.

When it comes to testing this, we’ll just write a single test to check that the visibility toggle composable reflects the expected state. We’ll need to start here by adding a new tag to our Tags object so that we can locate and interact with the visibility composable. We’ll end this tag with an underscore so that we can append the current boolean value of the toggle to the tag, meaning that we can locate the tag based on the enabled state of the toggle.

// Tags.kt

object Tags {
	...
    const val TAG_PASSWORD_HIDDEN = "password_hidden_"
}

Next, we’ll need to assign this tag our composable using the testTag function. When doing this we’ll also append the current value of our isPasswordHidden state, so that we can locate the node using this value. We do this as if the value is not aligned as expected, then the node won’t be found and the tests will fail.

// PasswordInput.kt

trailingIcon = {
	Icon(
		modifier = Modifier.testTag(TAG_PASSWORD_HIDDEN + isPasswordHidden),
		...
    )
}

With this in place, we can now start working on the test. Here we’ll begin by composing the PasswordInput with a string value for the password argument.

@Test
fun Password_Toggled_Reflects_state() {
    composeTestRule.setContent {
        PasswordInput(
            password = "password123",
            onPasswordChanged = {  },
            onDoneClicked = { }
        )
    }
}

We’ll then want to locate the visibility toggle composable. By default the visibility flag will be true, signifying that the password is hidden. Here we’re going to locate the node with the value of true appended to the tag, click on it and then assert that the tag with the value of false appended to the tag is displayed. Here we’re going to start by locating the node for the hidden state of our visibility toggle composable – we’ll need this so that we can perform a click interaction on the composable. Here we’ll use the TAG_PASSWORD_HIDDEN tag, appending the value of true on the end to match the expected condition of the state for the password visibility.

@Test
fun Password_Toggled_Reflects_state() {
    composeTestRule.setContent {
        PasswordInput(
            password = "password123",
            onPasswordChanged = {  },
            onDoneClicked = { }
        )
    }
    composeTestRule
        .onNodeWithTag(TAG_PASSWORD_HIDDEN + "true")
        .performClick()
}

With this in place, we are not performing a click interaction on the composable, this means that now the composable state should have recomposed the visibility toggle composable. This means that now, a node with the TAG_PASSWORD_HIDDEN tag for the visible state of the password should be visible. We can assert this here using the assertIsDisplayed function on the located node.

@Test
fun Password_Toggled_Reflects_state() {
    composeTestRule.setContent {
        PasswordInput(
            password = "password123",
            onPasswordChanged = {  },
            onDoneClicked = { }
        )
    }
    composeTestRule
        .onNodeWithTag(TAG_PASSWORD_HIDDEN + "true")
        .performClick()

    composeTestRule
        .onNodeWithTag(TAG_PASSWORD_HIDDEN + "false")
        .assertIsDisplayed()
}

While we could have a separate test here to assert that the true condition is displayed, this test covers both scenarios. This is because the first onNodeWithTag call will fail the test if the node is not found – this will mean that the tag for the hidden state of the visibility toggle would not currently be being displayed on the screen. Because this test requires the hidden state of the visibility toggle to perform the assertIsDisplayed assertion, we cover both scenarios in a single test.


Testing the Password Requirements

While we’re verifying the entry of a password from the tests above, our user is still required to enter a password that meets the minimum requirements that are defined within our ViewModel. These requirements are communicated to the user via the PasswordRequirements composable, with the composable containing logic to depict the message to be displayed based on the provided requirement statuses. So here, we’ll write some tests here to verify that this composable is operating as expected.

// PasswordRequirementsTest.kt

class PasswordRequirementsTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    
}

We’re first going to write a test to assert that each of the password requirements is displayed as expected. To keep things simple here and avoid needing to write multiple test conditions, we’re going to write a test that will assign random password requirements as satisfied. This way we can assert that expected requirements are displayed as both satisfied and unsatisfied.

We’re going to start here by retrieving the list of available requirements from our PasswordRequirement type, along with getting a random item from this list to be used as the satisfied requirement.

val requirements = PasswordRequirement.values().toList()
val satisfiedRequirement = requirements[(0 until requirements.count()).random()]

We could use a random number of satisfied requirements to vary the number between tests, but we’ll use a single one here to keep things simple for examples sake. We’ll next compose a PasswordRequirements, providing a list for the satisfiedRequirements argument that consists of the random requirement that we retrieved above, satisfiedRequirement.

@Test
fun Password_Requirements_Displayed_As_Not_Satisfied() {
    val requirements = PasswordRequirement.values().toList()
	val satisfiedRequirement = requirements[(0 until requirements.count()).random()]

	composeTestRule.setContent {
        PasswordRequirements(
            satisfiedRequirements = listOf(satisfied)
        )
    }
}

When the PasswordRequirements is composed, it should be the case that the requirements are composed based on the satisfied requirements that are provided. To test this we’re going to need to start by looping through each of the available PasswordRequirement values:

PasswordRequirement.values().forEach {
        
}

Next, we’ll use each of their labels, along with the provided satisfiedRequirement to build the string that we’re going to assert for. The PasswordRequirements composable is formatting two different string representations based on the satisfied state of each. If a requirement is marked as satisfied, then the password_requirement_satisfied resource is used to build a string for that requirement, otherwise the password_requirement_needed is used. Here for each of the requirements in the loop, we’re going to retrieve the string for the label of the requirement, along with building a string based on whether the requirement in the loop matches the satisfiedRequirement that we configured earlier in the test.

PasswordRequirement.values().forEach { requirement ->
	val requirement =
		InstrumentationRegistry.getInstrumentation()
			.context.getString(it.label)

	val result = if (requirement == satisfiedRequirement) {
		InstrumentationRegistry.getInstrumentation()
			.context.getString(
				R.string.password_requirement_satisfied, 
					requirement)
    } else {
        InstrumentationRegistry.getInstrumentation()
			.context.getString(
				R.string.password_requirement_needed, 
					requirement)
    }
}

Now, this string is being built, we can use this to locate a node and perform an assertion to ensure that it is being displayed within the composable.

PasswordRequirement.values().forEach { requirement ->
	val requirement =
		InstrumentationRegistry.getInstrumentation()
			.context.getString(it.label)
	val result = if (requirement == satisfiedRequirement) {
		InstrumentationRegistry.getInstrumentation()
			.context.getString(
				R.string.password_requirement_satisfied, 
					requirement)
    } else {
        InstrumentationRegistry.getInstrumentation()
			.context.getString(
				R.string.password_requirement_needed, 
					requirement)
    }

    composeTestRule
        .onNodeWithText(result)
        .assertIsDisplayed()
}

With this loop, our test is now looping through each of the availablePasswordRequirement values and asserting that the expected requirement message is displayed within the composable.

@Test
fun Password_Requirements_Displayed_With_State() {

    val requirements = PasswordRequirement.values().toList()
    val satisfied = requirements[(0 until 3).random()]

    composeTestRule.setContent {
        PasswordRequirements(
            satisfiedRequirements = listOf(satisfied)
        )
    }
    PasswordRequirement.values().forEach {
        val requirement =
			InstrumentationRegistry.getInstrumentation()
				.context.getString(it.label)
        val result = if (it == satisfied) {
            InstrumentationRegistry.getInstrumentation()
				.context.getString(
					R.string.password_requirement_satisfied, 
					requirement)
        } else {
            InstrumentationRegistry.getInstrumentation()
				.context.getString(
					R.string.password_requirement_needed, 
					requirement)
        }

        composeTestRule
            .onNodeWithText(result)
            .assertIsDisplayed()
    }
}

Testing the Error Dialog

Within our collection of composables for the authentication screen, we also have the AuthenticationErrorDialog that is used to display errors to the user. While this only features two arguments that are used to display and dismiss the error, these are key to the operation of the dialog, so we’ll add some tests to assert that these operate as expected. These tests will live inside of a new test class, AuthenticationErrorDialogTest.

class AuthenticationErrorDialogTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    
}

We’ll start here by writing a test that will be used to assert that the provided error message is displayed within our dialog as expected. Here we’ll need to define a new test that will be used to house this test logic, composing an AuthenticationErrorDialog that will be composed using the provided error reference. With this composition in place, we can then use our text rule to assert that there is a node displayed that has the exact text being provided via the error argument.

@Test
fun Error_Displayed() {
    val error = "This is an error"
    composeTestRule.setContent {
        AuthenticationErrorDialog(
            error = error,
            dismissError = { }
        )
    }
    composeTestRule
        .onNodeWithText(error)
        .assertTextEquals(error)
}

When it comes to dismissing the dialog, the AuthenticationErrorDialog composable takes a dismissError argument that is used to notify the parent composable that dismissal has been requested and our state needs to be updated. If this was broken for some reason, then the user wouldn’t be able to dismiss the dialog and be unable to perform authentication.

Similar to other tests that we’ve written for these composables, we’re going to use mockito to provide a mock implementation of our dismissError, followed by verifying that the lambda has been invoked when expected. After composing this AuthenticationErrorDialog and providing the required arguments, we can trigger the dismissal by clicking the node the has the error_action string resource assigned to it. When this is clicked, it is expected that the dismissal lambda will be triggered. So here we’ll use the verify function from mockito to verify that the dismissError lambda has been triggered as expected.

@Test
fun Dismiss_triggered_from_action() {
    val dismissError: () -> Unit = mock()
    composeTestRule.setContent {
        AuthenticationErrorDialog(
            error = "This is an error",
            dismissError = dismissError
        )
    }
    composeTestRule
        .onNodeWithText(
            InstrumentationRegistry.getInstrumentation()
				.context.getString(R.string.error_action)
        )
        .performClick()

    verify(dismissError).invoke()
}

With all of these tests in place, we’ve covered a lot of different cases that help to ensure our UI is working as expected. We’ve not only tested that composables are being composed based on the information that they are provided with, but also that they triggered the expected callbacks and trigger state manipulations within our composables. While the tests here aren’t extensive, we’ve been able to learn not only what options are available to us while testing composables, but also the approaches that we can take when doing so.

Leave a Reply

Your email address will not be published. Required fields are marked *