how to run Espresso tests against cloud phones
how to run Espresso tests against cloud phones
if you want to run Espresso tests on a cloudf.one cloud phone, the workflow is essentially “Android Studio without the USB cable.” Espresso is an instrumentation framework. it ships inside your test APK, runs in the same process as the app under test, and reports results back via the AndroidJUnitRunner. all of that is device-agnostic, which means any device reachable over ADB can run Espresso tests, including cloud phones connected via ADB-over-network.
cloud phones are particularly good for Espresso because Espresso is most useful on real-device behaviors: gestures, animations, scroll inertia, soft-keyboard interactions, accessibility events. these all emit on real arm64 hardware and behave inconsistently on emulators.
why Espresso on cloud phones beats Espresso on emulators
Espresso runs in-process with the app under test, which gives it sub-frame precision on view-hierarchy assertions. that precision is its strength when the device behaves predictably. emulators add unpredictability via x86 translation layers and host-OS scheduler noise. cloud phones eliminate that noise because they are real Samsung-class hardware running real Android kernels.
three categories benefit most. animation-heavy UIs (Espresso’s idling-resource model handles real animations cleanly). multi-window edge cases (split-screen, picture-in-picture). accessibility tests (TalkBack and Switch Access behave correctly only on real devices).
for context on the test-architecture choice, see real device cloud phones for mobile app testing and cloud phone vs physical Android device.
prerequisites
your Android project should already have the Espresso dependencies. if not, add them:
// app/build.gradle.kts
dependencies {
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1")
androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test:rules:1.5.0")
}
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
on your laptop or CI runner: Java 17, Android platform-tools (for adb), the Android SDK, and Gradle.
step 1: connect to your cloud phone
log in to cloudf.one and copy the ADB endpoint for your test phone.
adb connect adb-sg.cloudf.one:5555
adb devices
the device should show up with the network endpoint as its serial. our ADB setup guide covers troubleshooting.
step 2: run Espresso tests via Gradle
with one device connected, the simplest run is the standard Gradle command:
./gradlew :app:connectedAndroidTest
Gradle reads the connected device list, builds the test APK and the app APK, installs both on the cloud phone, and runs the tests. results land in app/build/reports/androidTests/connected/.
if you have multiple devices connected (cloud phones, USB devices, emulators), pin Espresso to the cloud phone:
./gradlew :app:connectedAndroidTest \
-PandroidTestSerial=adb-sg.cloudf.one:5555
step 3: write your first Espresso test
create app/src/androidTest/java/com/example/SmokeTest.kt:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SmokeTest {
@get:Rule
val rule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun loginButton_isVisible() {
onView(withId(R.id.loginButton)).check(matches(isDisplayed()))
}
@Test
fun loginFlow_succeeds() {
onView(withId(R.id.usernameField)).perform(typeText("test"))
onView(withId(R.id.passwordField)).perform(typeText("test123"))
onView(withId(R.id.loginButton)).perform(click())
onView(withId(R.id.welcomeText)).check(matches(withText("Welcome, test")))
}
}
run:
./gradlew :app:connectedAndroidTest -PandroidTestSerial=adb-sg.cloudf.one:5555
results appear in HTML form at app/build/reports/androidTests/connected/index.html.
step 4: handle idling resources for async work
Espresso’s killer feature is the idling-resource API. it lets your tests wait for async work without Thread.sleep.
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.idling.CountingIdlingResource
class NetworkIdler {
private val idler = CountingIdlingResource("network")
fun start() { idler.increment() }
fun stop() { idler.decrement() }
fun register() { IdlingRegistry.getInstance().register(idler) }
}
call start() when the network call begins and stop() when it completes. Espresso blocks any UI assertion until the idler reports idle. this works reliably on cloud phones because real-device timing is consistent.
step 5: run instrumented tests against multiple cloud phones in parallel
Gradle by default runs against one device. for parallel execution across many cloud phones, use AndroidX ManagedDevices or third-party tools like Spoon or Composer.
with Gradle managed devices and cloud phones, the simplest pattern is to define multiple Gradle tasks, each pinned to a different device serial:
# in CI, parallelize via shell
adb connect adb-sg.cloudf.one:5555
adb connect adb-sg.cloudf.one:5556
adb connect adb-sg.cloudf.one:5557
# shard the test suite 3 ways
./gradlew connectedAndroidTest -PandroidTestSerial=adb-sg.cloudf.one:5555 \
-Pshard=0 -PshardCount=3 &
./gradlew connectedAndroidTest -PandroidTestSerial=adb-sg.cloudf.one:5556 \
-Pshard=1 -PshardCount=3 &
./gradlew connectedAndroidTest -PandroidTestSerial=adb-sg.cloudf.one:5557 \
-Pshard=2 -PshardCount=3 &
wait
cloudf.one’s REST API gives you a list of locked devices. wire the lock loop into your CI step; see CI/CD integration for the full GitHub Actions example.
step 6: capture screenshots and screen recordings
for failure debugging, capture screenshots and a video.
@RunWith(AndroidJUnit4::class)
class ScreenshotTest {
@get:Rule
val screenshotRule = ScreenshotOnFailureRule()
// your tests
}
or use the AndroidX androidx.test.runner.screenshot.Screenshot API in a JUnit TestWatcher rule. for video recordings, use ADB:
adb -s adb-sg.cloudf.one:5555 shell screenrecord --time-limit 180 /sdcard/test.mp4
adb -s adb-sg.cloudf.one:5555 pull /sdcard/test.mp4 artifacts/
Espresso vs UiAutomator on cloud phones
Espresso runs in-process with your app and only sees views inside your app’s process. UiAutomator runs out-of-process and sees the entire system UI. for app-internal tests, use Espresso. for cross-app flows (notification, settings, system dialogs), use UiAutomator. both work identically on cloud phones because both ride on ADB-over-network.
common pitfalls
three issues catch teams new to Espresso on cloud phones.
first, soft keyboard. Espresso typeText() does not auto-dismiss the keyboard. use closeSoftKeyboard() if subsequent assertions fail because the keyboard is covering the view.
second, animations. disable system animations on the cloud phone for stable runs:
adb -s adb-sg.cloudf.one:5555 shell settings put global window_animation_scale 0
adb -s adb-sg.cloudf.one:5555 shell settings put global transition_animation_scale 0
adb -s adb-sg.cloudf.one:5555 shell settings put global animator_duration_scale 0
third, the test orchestrator. for clean test isolation, use the AndroidX test orchestrator. it runs each test in its own process, preventing shared-state bugs.
the official Espresso docs cover edge cases.
try Espresso on a real Singapore cloud phone
register for a free trial for a one-hour cloud phone token. that’s enough to run a full smoke suite end to end. once confirmed, scale to a paid plan and add cloud phones to your test pool.
frequently asked questions
does Espresso work over a slow network?
Espresso runs in-process on the device, so the test logic itself does not depend on network latency. ADB-over-network only matters for the install and the result reporting, which happen at the start and end. the test loop itself is fast even from outside Singapore.
can I run Espresso tests on multiple Android versions in parallel?
yes. tag cloud phones in cloudf.one by Android version (android-13, android-14, android-15). lock one phone per version in CI and run the same test suite against all of them simultaneously. our Android version coverage post covers fleet sizing.
what about Robolectric vs Espresso?
Robolectric runs in a JVM with stubbed Android APIs. it is fast but does not catch real-device bugs. Espresso runs on a real device and catches them. use both: Robolectric for unit tests, Espresso for instrumented tests on cloud phones.
how do I debug a flaky Espresso test on a cloud phone?
enable Espresso’s verbose logging, capture a screen recording during the run, and inspect logcat. the cloud phone’s session is persistent during your lock, so you can keep the phone state and re-run interactively.
does Espresso work with Jetpack Compose?
yes. add the androidx.compose.ui:ui-test-junit4 dependency and use composeTestRule alongside Espresso rules. both work on cloud phones identically.