TestKase Docs
AutomationTest Frameworks

Espresso

Set up Espresso for Android UI testing and integrate test results with TestKase.

Overview

Espresso is Android's native UI testing framework, part of the AndroidX Test library. Tests are written in Kotlin or Java and run directly on Android devices or emulators, providing fast and reliable UI interaction with automatic synchronization of the UI thread.

To integrate Espresso results with TestKase, use --format junit with the TestKase reporter. Gradle's connectedAndroidTest task automatically generates JUnit XML output.

Prerequisites

  • Android Studio (latest stable recommended)
  • Android SDK with platform tools
  • Java 11+
  • Gradle (bundled with Android Studio)
  • An Android emulator or physical device with USB debugging enabled

Installation

Add the Espresso dependencies to your app/build.gradle:

// app/build.gradle
android {
    defaultConfig {
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}

dependencies {
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation 'androidx.test:runner:1.5.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test:rules:1.5.0'
}

Project Setup

build.gradle Configuration

A complete app/build.gradle for Espresso testing:

// app/build.gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.example.myapp'
    compileSdk 34

    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.11.0'

    // Espresso and AndroidX Test
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation 'androidx.test:runner:1.5.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test:rules:1.5.0'
}

Directory Structure

Instrumented test files go in the androidTest source set:

app/
  src/
    main/java/com/example/myapp/       # Application code
    androidTest/java/com/example/myapp/ # Espresso tests

Writing Tests

Create a test class in the androidTest directory:

// app/src/androidTest/java/com/example/myapp/LoginTest.kt
package com.example.myapp

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
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 LoginTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun test_48271_ValidLogin() {
        onView(withId(R.id.emailInput))
            .perform(typeText("user@example.com"), closeSoftKeyboard())

        onView(withId(R.id.passwordInput))
            .perform(typeText("password123"), closeSoftKeyboard())

        onView(withId(R.id.loginButton))
            .perform(click())

        onView(withId(R.id.dashboardTitle))
            .check(matches(isDisplayed()))
    }

    @Test
    fun test_48272_InvalidPassword() {
        onView(withId(R.id.emailInput))
            .perform(typeText("user@example.com"), closeSoftKeyboard())

        onView(withId(R.id.passwordInput))
            .perform(typeText("wrong"), closeSoftKeyboard())

        onView(withId(R.id.loginButton))
            .perform(click())

        onView(withId(R.id.errorMessage))
            .check(matches(withText("Invalid credentials")))
    }

    @Test
    fun test_48273_EmptyEmailValidation() {
        onView(withId(R.id.loginButton))
            .perform(click())

        onView(withId(R.id.emailError))
            .check(matches(withText("Email is required")))
    }
}

Link 5-digit Automation IDs from TestKase to each test. Since Kotlin/Java method names cannot contain brackets, embed the ID in the method name using underscores (e.g., test_48271_ValidLogin) and configure the reporter with --automation-id-format "_(\\d{5})_", or use JUnit5's @DisplayName("[48271] ...").

For the tests above, generate IDs in TestKase and link them:

  • 48271 → linked to the "valid login" test case in TestKase
  • 48272 → linked to the "invalid password" test case in TestKase
  • 48273 → linked to the "empty email" test case in TestKase

Generate Automation IDs in TestKase first. For JUnit4 frameworks like Espresso where method names cannot contain brackets, use the --automation-id-format CLI flag to customize the extraction regex.

Running Tests

Run all instrumented tests with Gradle:

./gradlew connectedAndroidTest

For verbose output:

./gradlew connectedAndroidTest --info

Gradle generates JUnit XML results in:

app/build/outputs/androidTest-results/connected/

Each device produces a separate XML file (e.g., TEST-emulator-5554 - 14-com.example.myapp.LoginTest.xml).

To run tests for a specific class, use the -Pandroid.testInstrumentationRunnerArguments.class flag: ./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.myapp.LoginTest

TestKase Integration

After tests complete and the JUnit XML files are generated, report results to TestKase:

npx @testkase/reporter report \
  --token $TESTKASE_PAT \
  --project-id PRJ-1 \
  --org-id 1173 \
  --cycle-id TCYCLE-5 \
  --format junit \
  --automation-id-format "_(\\d{5})_" \
  --results-file "app/build/outputs/androidTest-results/connected/*.xml"

--cycle-id is optional. If not provided, results are reported to TCYCLE-1 — the master test cycle for the project.

Automation ID Mapping

The reporter extracts Automation IDs based on the configured pattern. For Espresso, embed the 5-digit ID in the method name and use a custom --automation-id-format:

ApproachTest MethodCLI FlagExtracted ID
Underscore patternfun test_48271_ValidLogin()--automation-id-format "_(\\d{5})_"48271
Default bracket patternRequires JUnit5 @DisplayName("[48271] ...")(default)48271

Generate the 5-digit ID in TestKase first, then embed it in your test method name or annotation.

Complete Example

1. Test File

// app/src/androidTest/java/com/example/myapp/LoginTest.kt
package com.example.myapp

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
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 LoginTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun test_48271_ValidLogin() {
        onView(withId(R.id.emailInput))
            .perform(typeText("user@example.com"), closeSoftKeyboard())

        onView(withId(R.id.passwordInput))
            .perform(typeText("password123"), closeSoftKeyboard())

        onView(withId(R.id.loginButton))
            .perform(click())

        onView(withId(R.id.dashboardTitle))
            .check(matches(isDisplayed()))
    }

    @Test
    fun test_48272_InvalidPassword() {
        onView(withId(R.id.emailInput))
            .perform(typeText("user@example.com"), closeSoftKeyboard())

        onView(withId(R.id.passwordInput))
            .perform(typeText("wrong"), closeSoftKeyboard())

        onView(withId(R.id.loginButton))
            .perform(click())

        onView(withId(R.id.errorMessage))
            .check(matches(withText("Invalid credentials")))
    }
}

2. Run Tests

# Ensure an emulator is running or a device is connected
./gradlew connectedAndroidTest

3. Report Results to TestKase

npx @testkase/reporter report \
  --token $TESTKASE_PAT \
  --project-id PRJ-1 \
  --org-id 1173 \
  --cycle-id TCYCLE-5 \
  --format junit \
  --automation-id-format "_(\\d{5})_" \
  --results-file "app/build/outputs/androidTest-results/connected/*.xml"

Troubleshooting

No connected devices

Espresso tests require a running emulator or a connected physical device. Start an emulator from Android Studio or the command line:

emulator -avd Pixel_6_API_34

For a physical device, enable USB debugging in Developer Options and verify the connection:

adb devices

You should see your device listed with a device status (not unauthorized or offline).

Test runner not found

If you see No tests found or a runner-related error, verify that testInstrumentationRunner is set correctly in your app/build.gradle:

android {
    defaultConfig {
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}

Also confirm that the test class is in the androidTest source set (not the test source set, which is for local unit tests).

Espresso IdlingResource for async operations

If tests fail because the UI has not finished loading, you need to register an IdlingResource so Espresso knows to wait for asynchronous work to complete:

// Register before tests
IdlingRegistry.getInstance().register(myIdlingResource)

// Unregister after tests
IdlingRegistry.getInstance().unregister(myIdlingResource)

Common scenarios that require idling resources include network calls, database operations, and animations. You can use CountingIdlingResource for tracking multiple concurrent async tasks:

val idlingResource = CountingIdlingResource("NetworkCalls")

// Increment when starting async work
idlingResource.increment()

// Decrement when async work completes
idlingResource.decrement()