Espresso tests run on a real device or emulator and interact with the actual UI. Robolectric simulates the Android environment on the JVM, allowing tests that need Android APIs to run without a device.
Espresso: Real Device UI Tests
Espresso provides a fluent API to find views, perform actions, and assert their state:
// androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
// androidTestImplementation("androidx.test:rules:1.5.0")
@RunWith(AndroidJUnit4::class)
class LoginActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Test
fun validCredentialsShowsHomeScreen() {
// Find view → perform action
onView(withId(R.id.email_field))
.perform(click(), typeText("user@example.com"), closeSoftKeyboard())
onView(withId(R.id.password_field))
.perform(click(), typeText("password123"), closeSoftKeyboard())
onView(withId(R.id.login_button))
.perform(click())
// Assert
onView(withText("Welcome!"))
.check(matches(isDisplayed()))
}
@Test
fun emptyPasswordShowsError() {
onView(withId(R.id.email_field))
.perform(typeText("user@example.com"), closeSoftKeyboard())
onView(withId(R.id.login_button))
.perform(click())
onView(withText("Password is required"))
.check(matches(isDisplayed()))
}
}
Compose UI Tests (Espresso alternative for Compose)
// androidTestImplementation("androidx.compose.ui:ui-test-junit4")
@RunWith(AndroidJUnit4::class)
class ArticleScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun articleDisplaysCorrectly() {
val testArticle = Article("a1", "Test Article", "Body text")
composeTestRule.setContent {
ArticleScreen(article = testArticle, onBack = {})
}
composeTestRule.onNodeWithText("Test Article").assertIsDisplayed()
composeTestRule.onNodeWithText("Body text").assertIsDisplayed()
}
@Test
fun backButtonCallsOnBack() {
var backCalled = false
composeTestRule.setContent {
ArticleScreen(article = testArticle, onBack = { backCalled = true })
}
composeTestRule.onNodeWithContentDescription("Navigate back").performClick()
assertTrue(backCalled)
}
}
Robolectric: Android APIs on the JVM
Robolectric shadows Android classes so you can test Android-dependent code without a device. Tests run in seconds instead of minutes:
// testImplementation("org.robolectric:robolectric:4.12.2")
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [33])
class NotificationHelperTest {
@Test
fun `notification has correct title and channel`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val helper = NotificationHelper(context)
val notification = helper.buildArticleNotification("New Article!")
assertEquals("New Article!", notification.extras.getString(Notification.EXTRA_TITLE))
assertEquals("articles_channel", notification.channelId)
}
@Test
fun `notification channel created on first use`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val helper = NotificationHelper(context)
helper.createNotificationChannel()
val manager = context.getSystemService(NotificationManager::class.java)
val channel = manager.getNotificationChannel("articles_channel")
assertNotNull(channel)
assertEquals(NotificationManager.IMPORTANCE_DEFAULT, channel.importance)
}
}
When to Use Each Tool
| Tool | Device needed | Speed | Use for |
|---|---|---|---|
| JUnit (pure) | No | Fast (~ms) | Business logic, non-Android code |
| Robolectric | No | Medium (~100ms) | Android APIs (Context, Notifications, Intents) without device |
| Compose UI Test (local) | No | Medium | Compose composables in isolation |
| Espresso | Yes (emulator/device) | Slow (~30s) | Full screen flows, real user interactions |
Common Espresso Patterns
// Scroll to an item in RecyclerView
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(10))
// Check item in RecyclerView
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click()))
// Handle dialogs
onView(withText("Confirm")).inRoot(isDialog()).perform(click())
// Wait for idle (Espresso does this automatically with IdlingResources)
onView(withId(R.id.content)).check(matches(isDisplayed()))
Key Takeaways
| Rule | Why |
|---|---|
| Prefer Robolectric over Espresso | Runs without device; 10-100× faster |
| Use Espresso for full flows | Login → home → detail navigations that span multiple activities |
ActivityScenarioRule | Modern replacement for ActivityTestRule; handles lifecycle correctly |
composeTestRule.setContent | Tests composables in isolation — no need for a full Activity |
| Espresso IdlingResource | Register async operations so Espresso waits for them before asserting |