What You'll Learn
Robot pattern (test structuring, Page Object-like design, Compose UI testing, custom assertions, test readability improvement) explained.
What is the Robot Pattern?
Separates actions (what to do) from assertions (what to verify) in UI tests. Dramatically improves test readability and maintainability.
Robot Definition
class LoginRobot(private val rule: ComposeTestRule) {
fun enterEmail(email: String) = apply {
rule.onNodeWithTag("email_field").performTextInput(email)
}
fun enterPassword(password: String) = apply {
rule.onNodeWithTag("password_field").performTextInput(password)
}
fun clickLogin() = apply {
rule.onNodeWithTag("login_button").performClick()
}
fun assertErrorShown(message: String) = apply {
rule.onNodeWithText(message).assertIsDisplayed()
}
fun assertLoginSuccess() = apply {
rule.onNodeWithTag("home_screen").assertIsDisplayed()
}
fun assertLoginButtonEnabled() = apply {
rule.onNodeWithTag("login_button").assertIsEnabled()
}
fun assertLoginButtonDisabled() = apply {
rule.onNodeWithTag("login_button").assertIsNotEnabled()
}
}
Test Implementation
@HiltAndroidTest
class LoginScreenTest {
@get:Rule val composeRule = createComposeRule()
private fun loginRobot() = LoginRobot(composeRule)
@Before
fun setup() {
composeRule.setContent {
LoginScreen(viewModel = FakeLoginViewModel())
}
}
@Test
fun validCredentials_navigatesToHome() {
loginRobot()
.enterEmail("test@example.com")
.enterPassword("password123")
.clickLogin()
.assertLoginSuccess()
}
@Test
fun invalidEmail_showsError() {
loginRobot()
.enterEmail("invalid")
.enterPassword("password123")
.clickLogin()
.assertErrorShown("Invalid email format")
}
@Test
fun emptyFields_loginButtonDisabled() {
loginRobot()
.assertLoginButtonDisabled()
}
}
Multi-Screen Robot Chaining
class HomeRobot(private val rule: ComposeTestRule) {
fun clickSettings() = SettingsRobot(rule).also {
rule.onNodeWithTag("settings_button").performClick()
}
fun assertUserName(name: String) = apply {
rule.onNodeWithText(name).assertIsDisplayed()
}
}
class SettingsRobot(private val rule: ComposeTestRule) {
fun clickLogout() = LoginRobot(rule).also {
rule.onNodeWithTag("logout_button").performClick()
}
fun toggleDarkMode() = apply {
rule.onNodeWithTag("dark_mode_switch").performClick()
}
}
// Test including screen transitions
@Test
fun logoutFlow() {
loginRobot()
.enterEmail("test@example.com")
.enterPassword("password")
.clickLogin()
.assertLoginSuccess()
HomeRobot(composeRule)
.assertUserName("Test User")
.clickSettings()
.clickLogout()
.assertErrorShown("") // Back to LoginScreen
}
LazyColumn Robot
class ItemListRobot(private val rule: ComposeTestRule) {
fun scrollToItem(index: Int) = apply {
rule.onNodeWithTag("item_list").performScrollToIndex(index)
}
fun clickItem(title: String) = apply {
rule.onNodeWithText(title).performClick()
}
fun assertItemCount(count: Int) = apply {
rule.onAllNodesWithTag("list_item").assertCountEquals(count)
}
fun swipeToDelete(title: String) = apply {
rule.onNodeWithText(title).performTouchInput { swipeLeft() }
}
}
Summary
| Element | Role |
|---|---|
| Robot | Action (operation) definition |
apply |
Method chaining |
| Assertion | Result verification |
| Screen transition | Robot→Robot return |
- Robot pattern dramatically improves test readability
-
applyenables method chaining (fluent API) - Screen transitions are type-safe by returning new Robot
- UI changes: modify Robot only, test body stays unchanged
Ready-Made Android App Templates
8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.
Browse templates → Gumroad
Top comments (0)