UI testing verifies the correctness of display and interaction of mobile application user interface elements — buttons, text fields, lists, and navigation components. Unlike unit tests that check business logic, UI tests emulate user actions: taps, swipes, text input, and verify the interface response. According to a study by Android Developers, 2024, UI testing covers 70% of critical user scenarios and helps identify layout defects that are not detectable through logical checks.
Key Takeaways
UI testing is a type of automated testing where test code interacts with the application’s graphical interface just as a real user would. The test finds an element on the screen — a button, text field, list — performs an action on it, and verifies the expected interface response. For example, after entering an incorrect password, a UI test checks that an error message with the correct text appears on the screen.
The main difference between UI tests and other types of automation is that they work through the operating system’s Accessibility layer rather than through the application’s internal APIs. This means UI tests see the interface exactly as a user and a screen reader would. This allows UI tests to verify not only functionality but also element accessibility — compliance with WCAG requirements.
According to the JetBrains Developer Ecosystem 2023 survey, 58% of mobile teams use UI tests in their CI/CD pipeline. The average UI test coverage in commercial projects is 30–40% of application screens. Projects with UI tests receive 25% fewer negative reviews in app stores related to interface crashes.
The main difference between UI tests and unit tests is the level of abstraction. Unit tests work with individual classes and functions isolated from the Android or iOS framework. They run on the JVM (for Android) without starting an emulator and take milliseconds. UI tests run on a real device or emulator, interact with system services, and take seconds or minutes per scenario.
The target audience of tests also differs. UI tests verify end-to-end user scenarios — registration, order placement, search. Unit tests cover business logic: calculations, validation, data transformation. A UI test does not verify the correctness of tax calculation — it checks that the final amount is displayed on the screen. The calculation itself is verified by a unit test.
According to Google Testing Blog (2020), the optimal test ratio in a project follows the testing pyramid rule: 70% unit tests, 20% integration tests, and 10% UI tests. Violating this proportion in favor of UI tests leads to increased run time and test suite fragility, since UI tests are sensitive to changes in screen layout.
For Android, the dominant framework is Espresso — a library from Google built into AndroidX Test. Espresso automatically synchronizes with the UI thread, waiting for animations and background tasks to complete before executing the next assertion. For Jetpack Compose, the Compose UI Test extension is used, which works through semantic nodes instead of traditional view identifiers.
For iOS, the primary tool is XCUITest, which is part of Xcode. Tests are written in Swift and use Accessibility identifiers to find elements. XCUITest supports test recording through the record function and integration with CI systems via xcodebuild. For cross-platform projects, Appium is used, based on the WebDriver protocol and allowing the same tests to run on Android and iOS with minimal code changes.
Espresso works with the traditional View system through onView and resource id identifiers. Compose UI Test uses a semantic layer, making tests less dependent on the view hierarchy. For example, finding a button in Espresso: onView(withId(R.id.submit)), in Compose: onNodeWithTag(“submit”). Compose tests automatically handle recomposition and do not require explicit idle state waits.
XCUITest uses XCUIApplication as the entry point. Each interface element is found through Accessibility properties: accessibilityIdentifier for programmatic access and accessibilityLabel for VoiceOver. The framework supports test recording through Xcode’s record function — the developer performs actions on the simulator, and Xcode generates test code. Ready tests are run via xcodebuild test.
Appium is based on the WebDriver protocol and supports any language: Java, Python, JavaScript. Element search strategies include id, xpath, class name, and accessibility id. Appium requires server installation and configuration of Desired Capabilities — platformName, deviceName, appPackage. An alternative is Maestro, which uses YAML scenarios and does not require test code compilation.
Let’s look at UI tests for the same scenario — logging into an application — using three different frameworks: Espresso for Android, XCUITest for iOS, and Appium for a cross-platform approach. Scenario: enter login and password, click the login button, verify the welcome message is displayed.
An Espresso test uses onView to find an element by its identifier and perform to execute an action. The check method with the isDisplayed matcher confirms that the element is visible on the screen.
@RunWith(AndroidJUnit4::class)
class LoginUiTest {
@Rule
@JvmField
val composeTestRule = createComposeRule()
@Test
fun login_withValidCredentials_showsWelcome() {
composeTestRule
.onNodeWithTag("emailField")
.performTextInput("user@example.com")
composeTestRule
.onNodeWithTag("passwordField")
.performTextInput("secret123")
composeTestRule
.onNodeWithTag("loginButton")
.performClick()
composeTestRule
.onNodeWithText("Welcome, User!")
.assertIsDisplayed()
}
}
XCUITest uses XCUIApplication to access interface elements through Accessibility identifiers. The tap() and exists methods provide interaction and verification.
class LoginUITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
continueAfterFailure = false
app.launch()
}
func testLogin_withValidCredentials_showsWelcome() {
app.textFields["emailField"].tap()
app.textFields["emailField"].typeText("user@example.com")
app.secureTextFields["passwordField"].tap()
app.secureTextFields["passwordField"].typeText("secret123")
app.buttons["loginButton"].tap()
XCTAssertTrue(app.staticTexts["Welcome, User!"].exists)
}
}
The first principle — use Accessibility identifiers instead of text labels for finding elements. Button text can change during localization, while the identifier remains stable. In Android, this is the contentDescription property; in iOS — accessibilityIdentifier. This approach makes tests independent of the interface language and reduces maintenance costs when copywriting changes.
Avoid sleep() and fixed delays — use the framework’s built-in waiting mechanisms. Espresso automatically waits for animations and background tasks to complete. XCUITest provides XCTAssertTrue with a timeout. Explicit pauses make tests slower and more unstable, especially on slow devices in a CI environment.
Group tests by criticality: smoke tests (3–5 key scenarios) run on every commit, the full UI test suite runs before release. According to Google Testing Blog (2022), UI tests that take more than 30 minutes in CI reduce run frequency by 40%, decreasing their effectiveness as an early regression detection tool.
UI tests have several limitations. Sensitivity to layout changes: changing an identifier, hierarchy, or element type breaks the test even when functionality remains unchanged. The solution is to use the Page Object pattern, which centralizes element selectors in separate classes. When the layout changes, only one Page Object file is modified, not dozens of tests.
Execution time: running on a real device or emulator takes 10–50 times longer than a unit test. The solution is to run UI tests in parallel on multiple devices using Firebase Test Lab or AWS Device Farm. Flakiness is a common CI run problem caused by animations, network delays, or emulator state. To combat flakiness, automatic retries of failed tests and stability analytics for each test scenario are used.
Frequently Asked Questions
For an average screen, 3–5 UI tests are sufficient: happy path, error validation, empty state, orientation change, and Accessibility check. Complex screens with multiple states — order forms, settings — may require 10–15 tests for full coverage of key scenarios.
Yes, Appium and Maestro allow running the same scenarios on both platforms. However, native frameworks — Espresso and XCUITest — provide better stability, speed, and access to platform-specific features that are not available through WebDriver proxies.
For Compose, the Compose UI Test library with semantic matchers is used: onNodeWithText, onNodeWithTag, onNodeWithContentDescription. Compose’s semantic layer abstracts the view hierarchy, making tests less fragile compared to traditional Espresso for the View system.
Basic UI test runs are performed on emulators in CI — it’s fast and cheap. Final verification before release should be done on physical devices through Firebase Test Lab to account for real hardware characteristics: different resolutions, OS versions, and performance.
Use parallel execution on multiple devices, disable animations on the emulator via Developer Options, build a modular test architecture, and run the smoke suite on every commit with the full regression run scheduled or before release.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also