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 enableAuthentication
argument. 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.