Kaspresso is one of those Android testing tools that sounds like another wrapper around Espresso.

And technically, yes, it is built on top of Espresso and UI Automator.

But that is also the point.

Espresso is powerful, but writing real test automation with plain Espresso can get annoying very fast. You end up with long matchers, repeated waits, flaky clicks, weak logs, screenshots bolted on later, and test code that looks like someone lost a fight with onView().

Kaspresso tries to fix that.

It gives you a cleaner Kotlin DSL, better logging, steps, screenshots, flaky-safety mechanisms, device controls, and access to both app UI and system UI. So you still run native Android instrumentation tests, but the test code becomes more readable and more useful for actual QA work.

Not magic. Not a silver bullet. But definitely less painful than raw Espresso.


What Kaspresso actually is

Kaspresso is a native Android UI testing framework.

Under the hood it uses:

PieceWhat it does
EspressoMain interaction with views inside your app
UI AutomatorInteraction with system UI and other apps
KakaoKotlin DSL over Espresso
KautomatorKotlin DSL over UI Automator
KaspressoTest structure, steps, logging, screenshots, flaky safety, device actions

So you can think of it like this:

Espresso = raw engine
Kakao = nicer syntax for Espresso
UI Automator = system/outside-app control
Kautomator = nicer syntax for UI Automator
Kaspresso = test framework around all of that

That is why Kaspresso feels closer to real QA automation than plain Espresso.

Espresso gives you the tools. Kaspresso gives you a workflow.


When I would use Kaspresso

I would use Kaspresso when I test an Android app and I want native UI tests that are readable, stable enough for CI, and not completely horrible to maintain.

Good use cases:

Use caseWhy Kaspresso fits
Login flowsEasy screen objects, steps, assertions
Checkout / order flowsBetter logs and screenshots when something breaks
App navigationClean DSL makes tests readable
Permission dialogsUI Automator/Kautomator can help with system UI
Flaky UI flowsKaspresso has flaky-safety tools
Regression smoke testsGood fit for CI on emulator/device
Native Android appsIt runs close to the app and Android framework

Bad use cases:

Use caseBetter tool
Testing iOS and Android from one frameworkAppium
Fast JVM-only unit testsRobolectric or plain JUnit
Business logic without UIJUnit/MockK
Testing backend/API onlyRestAssured/Postman/Newman/etc.
WebView-heavy cross-platform appMaybe Appium or Playwright, depends on the app

Kaspresso is not trying to replace every testing tool.

It is for Android UI/instrumentation tests.


Why use Kaspresso instead of plain Espresso?

Plain Espresso works. I am not saying it is bad.

But if you have written enough Espresso tests, you know the usual pain:

  • noisy onView(withId(...)) code everywhere
  • repeated matchers
  • weak reporting
  • screenshots are not built into your normal flow
  • logs are not friendly for debugging
  • flaky waits become your problem
  • test readability gets worse as the suite grows

Kaspresso improves the working experience.

ReasonEspressoKaspresso
SyntaxRaw onView() / matchersKotlin DSL via Kakao
ReadabilityCan become noisyScreen objects are cleaner
LoggingBasicBetter structured logs
ScreenshotsManual setupBuilt-in support
Flaky handlingMostly your jobFlaky-safety helpers
System UINeed UI Automator separatelyUI Automator is part of the ecosystem
Test stepsManual conventionBuilt into test style
QA friendlinessMore developer-styleMore test-automation-style

Example of Espresso style:

onView(withId(R.id.email))
    .perform(typeText("demo@test.com"), closeSoftKeyboard())

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

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

onView(withId(R.id.homeTitle))
    .check(matches(isDisplayed()))

Same idea with Kaspresso/Kakao screen objects:

LoginScreen {
    email.replaceText("demo@test.com")
    password.replaceText("password")
    loginButton.click()
}

HomeScreen {
    title.isDisplayed()
}

This is not just prettier. It matters when you have 100+ tests and need to understand failures fast.


Why use Kaspresso instead of Appium?

Appium is useful when you need cross-platform automation or black-box testing from outside the app.

But for native Android apps, Appium is often heavier than you need.

ReasonAppiumKaspresso
Test typeBlack-box, external driverNative Android instrumentation
SpeedUsually slowerUsually faster
SetupAppium server, capabilities, driversAndroid test dependency + Gradle
LanguageMany languagesKotlin/Java Android test code
Cross-platformAndroid + iOSAndroid only
Access to app internalsLimitedBetter, because tests run inside Android instrumentation
CI complexityHigherLower for Android-only projects
Best forCross-platform E2ENative Android regression/UI tests

My practical opinion:

Use Appium if the company wants one automation stack for Android and iOS.

Use Kaspresso if you are serious about Android and want tests that Android developers can also run and debug.

For Android-only UI tests, Kaspresso is usually the more sane choice.


Why use Kaspresso instead of Robolectric?

Robolectric is not the same kind of tool.

Robolectric runs Android-ish tests on the JVM. It is fast and useful for testing logic that touches Android classes without launching a real emulator.

Kaspresso runs on a real device or emulator as an instrumentation test.

ReasonRobolectricKaspresso
Runs onJVMEmulator/real device
SpeedFastSlower
UI realismLimitedReal Android UI
Good forViewModel-ish logic, Android framework interactionsReal user flows
System dialogsNot the real thingCan interact with real system UI
FlakinessLower, because no real deviceHigher risk, but more realistic
CI costCheapMore expensive
ConfidenceMediumHigher for actual UI behavior

Use Robolectric for fast feedback.

Use Kaspresso when you need to know the app actually works on Android.

They can live together.

A healthy Android test pyramid could look like this:

Many unit tests
Some Robolectric tests
Some Kaspresso UI tests
A few Appium E2E tests only if cross-platform is needed

Installing Kaspresso

In your app module, add the dependency to build.gradle.kts.

dependencies {
    androidTestImplementation("com.kaspersky.android-components:kaspresso:1.6.1")
}

Make sure you have an instrumentation runner:

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

You will also usually have AndroidX test dependencies already:

dependencies {
    androidTestImplementation("androidx.test.ext:junit:1.2.1")
    androidTestImplementation("androidx.test:runner:1.6.2")
    androidTestImplementation("androidx.test:rules:1.6.1")
}

Version numbers change. Check Maven Central before copying this into a real project six months from now.


Basic project structure

Kaspresso tests live in:

app/src/androidTest/java/your/package/name/

A simple structure:

androidTest/
โ””โ”€โ”€ java/
    โ””โ”€โ”€ com/example/app/
        โ”œโ”€โ”€ screens/
        โ”‚   โ”œโ”€โ”€ LoginScreen.kt
        โ”‚   โ””โ”€โ”€ HomeScreen.kt
        โ””โ”€โ”€ tests/
            โ””โ”€โ”€ LoginTest.kt

The important idea: keep UI locators in screen classes, not directly inside every test.

Bad:

@Test
fun loginWorks() {
    onView(withId(R.id.email)).perform(typeText("demo@test.com"))
    onView(withId(R.id.password)).perform(typeText("password"))
    onView(withId(R.id.loginButton)).perform(click())
    onView(withId(R.id.homeTitle)).check(matches(isDisplayed()))
}

Better:

@Test
fun loginWorks() = run {
    step("Enter login credentials") {
        LoginScreen {
            email.replaceText("demo@test.com")
            password.replaceText("password")
        }
    }

    step("Tap login") {
        LoginScreen {
            loginButton.click()
        }
    }

    step("Check home screen") {
        HomeScreen {
            title.isDisplayed()
        }
    }
}

That is the whole point: tests should read like actions, not like matcher soup.


Creating a screen object

Example LoginScreen.kt:

package com.example.app.screens

import com.example.app.R
import com.example.app.ui.LoginActivity
import com.kaspersky.kaspresso.screens.KScreen
import io.github.kakaocup.kakao.edit.KEditText
import io.github.kakaocup.kakao.text.KButton

object LoginScreen : KScreen<LoginScreen>() {

    override val layoutId: Int = R.layout.activity_login
    override val viewClass: Class<*> = LoginActivity::class.java

    val email = KEditText { withId(R.id.email) }
    val password = KEditText { withId(R.id.password) }
    val loginButton = KButton { withId(R.id.loginButton) }
}

Example HomeScreen.kt:

package com.example.app.screens

import com.example.app.R
import com.example.app.ui.HomeActivity
import com.kaspersky.kaspresso.screens.KScreen
import io.github.kakaocup.kakao.text.KTextView

object HomeScreen : KScreen<HomeScreen>() {

    override val layoutId: Int = R.layout.activity_home
    override val viewClass: Class<*> = HomeActivity::class.java

    val title = KTextView { withId(R.id.homeTitle) }
}

The exact imports can differ depending on your version and app setup, but the pattern is what matters.

Each screen class describes what exists on the screen.

The test describes what the user does.

Do not mix those two unless you want a maintenance mess later.


Writing a Kaspresso test

Example LoginTest.kt:

package com.example.app.tests

import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.app.screens.HomeScreen
import com.example.app.screens.LoginScreen
import com.example.app.ui.LoginActivity
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class LoginTest : TestCase() {

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

    @Test
    fun userCanLogin() = run {
        step("Enter credentials") {
            LoginScreen {
                email.replaceText("demo@test.com")
                password.replaceText("password")
            }
        }

        step("Submit login form") {
            LoginScreen {
                loginButton.click()
            }
        }

        step("Verify home screen is opened") {
            HomeScreen {
                title.isDisplayed()
            }
        }
    }
}

This is the style I like for UI tests.

Not too abstract. Not too clever. Not a page object religion.

Just enough structure so the test is readable.


Common commands

Here are the commands you will actually use.

TaskCommand
Run all connected Android tests./gradlew connectedDebugAndroidTest
Run tests for a specific build variant./gradlew connectedFreeDebugAndroidTest
Run one test class./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.app.tests.LoginTest
Run one test method./gradlew connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.app.tests.LoginTest#userCanLogin
Install debug APK./gradlew installDebug
Install test APK./gradlew installDebugAndroidTest
List connected devicesadb devices
Clear app dataadb shell pm clear com.example.app
Run instrumentation manuallyadb shell am instrument -w com.example.app.test/androidx.test.runner.AndroidJUnitRunner
Run instrumentation with one classadb shell am instrument -w -e class com.example.app.tests.LoginTest com.example.app.test/androidx.test.runner.AndroidJUnitRunner
Pull test artifacts from deviceadb pull /sdcard/Android/data/com.example.app/files/ ./test-artifacts
Check device logsadb logcat
Run on CI emulatorStart emulator, then run ./gradlew connectedDebugAndroidTest

The Gradle command is what you will use most of the time.

The adb shell am instrument command is useful when debugging or when your CI setup is custom.


Common Kaspresso/Kakao actions

This is the stuff you use every day.

ActionExample
Tap a buttonloginButton.click()
Type textemail.typeText("demo@test.com")
Replace textemail.replaceText("demo@test.com")
Clear textemail.clearText()
Check visibletitle.isDisplayed()
Check not visibleerrorText.isNotDisplayed()
Check texttitle.hasText("Welcome")
Check hintemail.hasHint("Email")
Scroll to elementsomeView.scrollTo()
Assert enabledloginButton.isEnabled()
Assert disabledloginButton.isDisabled()
Assert checkedcheckbox.isChecked()
Assert not checkedcheckbox.isNotChecked()

Example:

LoginScreen {
    email.replaceText("demo@test.com")
    password.replaceText("password")
    loginButton.click()
}

Simple. Readable. No ceremony.


Using steps properly

Kaspresso steps are not just decoration.

They make logs readable.

Bad:

@Test
fun checkoutWorks() = run {
    CatalogScreen { item.click() }
    CartScreen { checkout.click() }
    PaymentScreen { cardNumber.replaceText("4111111111111111") }
    PaymentScreen { pay.click() }
    SuccessScreen { title.isDisplayed() }
}

Better:

@Test
fun checkoutWorks() = run {
    step("Open product from catalog") {
        CatalogScreen {
            item.click()
        }
    }

    step("Go to checkout") {
        CartScreen {
            checkout.click()
        }
    }

    step("Enter payment details") {
        PaymentScreen {
            cardNumber.replaceText("4111111111111111")
            pay.click()
        }
    }

    step("Verify successful order") {
        SuccessScreen {
            title.isDisplayed()
        }
    }
}

When this fails in CI, you do not want to guess where it died.

Steps give you a readable story.


Using Kautomator for system UI

Sometimes your test leaves the app.

Examples:

  • Android permission dialogs
  • notification shade
  • system settings
  • another app
  • file picker
  • camera/gallery picker

This is where UI Automator comes in. Kaspresso includes Kautomator for a nicer DSL.

Example idea:

import com.kaspersky.components.kautomator.component.common.views.UiView

object PermissionDialog {
    val allowButton = UiView { withText("Allow") }
}

Then in a test:

step("Allow notification permission") {
    PermissionDialog.allowButton.click()
}

Text can change by Android version and language, so be careful. System UI tests are always more fragile than normal app UI tests.

If you can avoid system dialogs in most tests, avoid them.

For one smoke test, fine.

For every test, pain.


Handling flaky tests

Kaspresso has flaky-safety mechanisms that retry actions/assertions for a period of time instead of instantly failing on small UI timing issues.

That helps with real Android UI automation because apps are not perfectly synchronous.

But here is the important part:

Do not use flaky safety as an excuse to write bad tests.

If the app needs 10 seconds to show a button because the backend is slow, fix the test setup. Mock the backend, seed data, disable animations, or control the state.

Flaky safety should protect against small UI timing issues.

It should not hide broken architecture.

Practical anti-flake checklist:

ProblemBetter fix
Animation makes click unstableDisable animations on emulator
Backend data is randomUse mock server or seeded test data
Test depends on previous testClear app data before test
Same account reused by parallel testsGenerate isolated test users
System dialog appears randomlyHandle it in setup
Element selector is weakUse stable resource IDs
Test waits for text from networkMock or control the response

Useful emulator setup:

adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0

This alone removes a lot of nonsense.


Use this simple pattern:

Arrange
Act
Assert

For UI tests:

Prepare app state
Open screen
Perform user action
Check result

Example:

@Test
fun wrongPasswordShowsError() = run {
    before {
        // clear user/session state if needed
    }.after {
        // cleanup if needed
    }.run {
        step("Open login screen") {
            // activity rule already opened it
        }

        step("Enter invalid credentials") {
            LoginScreen {
                email.replaceText("demo@test.com")
                password.replaceText("wrong-password")
                loginButton.click()
            }
        }

        step("Verify error message") {
            LoginScreen {
                errorText.hasText("Invalid email or password")
            }
        }
    }
}

The point is not to make it fancy.

The point is to make it obvious.

A test should be boring to read.

Boring tests are usually good tests.


What to test with Kaspresso

Do not automate everything through the UI.

That is how teams create a slow and flaky monster.

Good Kaspresso test candidates:

TestWhy
Login successCritical path
Login errorCommon failure state
Main navigationApp health check
Checkout/order flowBusiness-critical
Settings changeReal UI state
Permission-dependent flowAndroid-specific behavior
Offline stateImportant user scenario
Deep link openingAndroid integration point

Bad Kaspresso test candidates:

TestWhy not
Every validation ruleBetter as unit tests
Every API errorBetter as API/contract tests
Every tiny UI labelToo brittle
Visual pixel-perfect checksUse screenshot testing tools
Complex backend workflowsMock or test lower layers
Pure ViewModel logicUnit test it

The UI suite should be small and valuable.

Not huge and miserable.


Kaspresso vs Espresso vs Appium vs Robolectric

Here is the practical comparison.

ToolBest forRuns onMain advantageMain downside
KaspressoNative Android UI regression testsEmulator/deviceReadable, stable-ish, great Android workflowAndroid only
EspressoNative Android UI testsEmulator/deviceOfficial, powerful, directVerbose, less QA-friendly
AppiumCross-platform E2EEmulator/device/real deviceAndroid + iOS from one stackSlower, heavier, more moving parts
RobolectricJVM Android-ish testsLocal JVMFast, cheap CI feedbackNot real UI/device behavior

My rule:

SituationPick
Android-only UI automationKaspresso
Small official Android UI test with no framework overheadEspresso
Android + iOS E2E from one suiteAppium
Fast tests without emulatorRobolectric
Business logicJUnit
API testingRestAssured
Web UI testingPlaywright

Tools are not religions.

Use the tool that matches the test layer.


CI setup idea

A basic CI flow:

name: Android UI Tests

on:
  pull_request:
  push:
    branches: [ main ]

jobs:
  ui-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - name: Run Android tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 35
          target: google_apis
          arch: x86_64
          script: ./gradlew connectedDebugAndroidTest

This is enough to start.

Later you can add:

  • screenshots as artifacts
  • Allure report
  • test retries
  • sharding
  • managed devices
  • Firebase Test Lab
  • different Android API levels

But do not start with a monster setup.

Make one reliable test run first.

Then expand.


Common mistakes

MistakeWhy it hurts
Putting all locators inside test methodsTest becomes impossible to maintain
Testing too much through UISuite becomes slow and flaky
Depending on real backendRandom failures
Reusing one test account everywhereParallel runs break
Ignoring test data setupTests become order-dependent
Using text selectors everywhereBreaks with copy changes/localization
Not disabling animationsRandom UI timing failures
No screenshots/logs in CIDebugging becomes archaeology
Making page objects too abstractNobody understands the test anymore

The worst automation suites are not bad because of the tool.

They are bad because nobody controlled the scope.

Kaspresso helps, but it will not save a chaotic test strategy.


My practical setup recommendation

If I were adding Kaspresso to a real Android project, I would start like this:

StepWhat I would do
1Add Kaspresso dependency
2Create 2-3 screen objects
3Write one happy-path smoke test
4Add one negative test
5Run locally from Gradle
6Run in CI on one emulator
7Save screenshots/logs as artifacts
8Add only business-critical flows

Start small.

One stable test is better than twenty flaky ones.

For the first batch, I would automate:

PriorityTest
P0App launches
P0User can log in
P0Main screen opens
P1Wrong password shows error
P1User can log out
P1One important business flow works

After that, stop and evaluate.

Are the tests stable? Are failures easy to debug? Is CI too slow? Are selectors good? Do developers actually run them?

If the answer is no, fix the foundation before writing more tests.


Final take

Kaspresso is what I would use when plain Espresso starts feeling too raw, but Appium feels too heavy.

It keeps you inside the Android ecosystem, gives you readable Kotlin test code, and adds the kind of things QA engineers actually need: steps, logs, screenshots, flaky handling, and system UI support.

It will not magically make bad tests good.

But if your Android app needs a real UI regression suite, Kaspresso is a very reasonable choice.

My honest recommendation:

Use Kaspresso for a small number of high-value Android UI tests.

Use unit tests and Robolectric for the lower layers.

Use Appium only when cross-platform coverage is actually required.

That setup gives you coverage without turning the test suite into a slow circus.