Ultron UI Testing Framework

repository·master·Indexed 42 days ago

https://github.com/open-tool/ultron

A simplified UI testing framework for Android and Compose Multiplatform that builds upon Espresso, UI Automator, and Compose UI testing frameworks. Ultron provides a more stable and expressive syntax, supports the Page Object pattern for maintainable tests, and includes built-in integration for Allure report generation.

Tokens
38.8K
Snippets
116
Records
136
Agent score
53%

What's inside Ultron

  1. Ultron library components

    master

    Ultron provides three main libraries depending on your testing target:

    • com.atiurin:ultron-compose: Used for both Android application and Compose Multiplatform UI tests.
    • com.atiurin:ultron-android: Provides native Android UI tests based on Espresso (including web components) and UI Automator.
    • com.atiurin:ultron-allure: Provides Allure report support specifically for Android application UI tests.
  2. Supported UI test types for Compose

    master

    Ultron supports two distinct types of Jetpack Compose UI tests, making development easier by providing unified support for both:

    1. Kotlin Multiplatform UI tests: For cross-platform Compose development. Refer to the official Kotlin documentation for core details.
    2. Platform Specific JUnit-based tests: Specifically for Android-based Compose testing. Refer to the official Android documentation for platform-specific details.
  3. How UltronRecyclerView works with RecyclerView

    master

    Ultron provides UltronRecyclerView as a wrapper around the standard Android RecyclerView to simplify interactions and assertions.

    Key concepts:

    • RecyclerView: The standard Android framework list class.
    • UltronRecyclerView: The Ultron wrapper used to perform actions and assertions on the list.
    • UltronRecyclerViewItem: Represents a single item within the list. You can interact with it directly or use subclasses to represent complex items with children.
    • RecyclerViewItem.child: A term for child elements within an item (essentially standard Android Views).
  4. What are UI Blocks and why use them?

    master

    UI Blocks are abstractions used to describe and interact with user interface elements within the context of their parent blocks rather than the entire screen.

    Benefits:

    • Readability: Tests focus on logical components rather than raw selectors.
    • Maintainability: Changes to a component's internal structure only require updating one UI Block class.
    • Reliability: Scoping searches to a parent block reduces the risk of interacting with the wrong element.
    • Reusability: A single UI Block (e.g., a ContactCard) can be defined once and reused across multiple screens.

    Ultron supports UI Blocks for:

    • Compose (CMP & Android)
    • Espresso
    • Espresso Web
    • UiAutomator (UiObject2 only)
  5. Manage test execution order with RuleSequence

    master

    RuleSequence is a replacement for JUnit 4's RuleChain that provides flexible control over the order in which rules are executed. It is designed to work well with class inheritance, allowing you to define a base sequence in a BaseTest class and extend it in subclasses.

    Rules are categorized into three priority lists:

    • first: Executed before all other rules.
    • normal: The default list for rules.
    • last: Executed after all other rules.

    When using RuleSequence, you only need to annotate the RuleSequence instance itself with @get:Rule. Other rules added to the sequence do not require the @get:Rule annotation.

    abstract class BaseTest {
        val setupRule = SetUpRule(name = "some name").add {
            // precondition for all tests
        }
    
        @get:Rule
        open val ruleSequence = RuleSequence(setupRule)
    }
    
    class DemoTest : BaseTest() {
        private val activityRule = ActivityScenarioRule(MainActivity::class.java)
    
        init {
            // Add rules to the sequence in the init block of subclasses
            ruleSequence.addLast(activityRule)
        }
    }
  6. Improve test stability with timeouts

    master

    Ultron improves stability by automatically retrying operations that encounter specified exceptions during a timeout period (default is 5 seconds). You can customize the timeout for any specific operation using .withTimeout(ms).

    withId(R.id.result).withTimeout(10_000).hasText("Passed")
  7. Convert operations to Boolean using isSuccess

    master

    The isSuccess method allows you to treat an action or assertion as a Boolean. Note that if an operation fails, it may wait for the default timeout (5 seconds). It is recommended to specify a custom timeout using withTimeout when using isSuccess to avoid long delays in your tests.

    val isWebElementExist = xpath("some_xpath").isSuccess { withTimeout(2_000).exists() }
    if (isWebElementExist) {
        // do some reasonable actions
    }
  8. Perform stable operations on SemanticsMatchers

    master

    In Ultron, instead of passing a SemanticsMatcher into a rule method, you call the operation directly on the matcher itself. This makes the code more concise and leverages Ultron's stability improvements for finding and interacting with nodes.

    // Instead of: composeTestRule.onNode(hasTestTag("Continue")).performClick()
    // Use:
    hasTestTag("Continue").click()
    
    // Instead of: composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
    // Use:
    hasText("Welcome").assertIsDisplayed()
  9. How Ultron handles Compose LazyLists

    master

    In Jetpack Compose, LazyColumn and LazyRow do not allow predicting the exact location of an item without scrolling. Ultron abstracts this complexity using three core concepts:

    1. ComposeList: A wrapper around a list (typically LazyColumn or LazyRow) implemented via the UltronComposeList class.
    2. ComposeListItem: A single item within a ComposeList, represented by the UltronComposeListItem class.
    3. ComposeListItemChild: A child element within an item (a standard Compose node).

    Ultron handles the scrolling logic automatically when you attempt to interact with items that are not currently in the viewport.

  10. Use `UltronTest` to manage test lifecycles

    master

    Extend the UltronTest base class to define common preconditions and postconditions for your test suite. This allows you to automate setup and teardown logic across multiple tests.

    Lifecycle Hooks

    • beforeFirstTest: Executed once before any tests in the class run.
    • beforeTest: Executed before every individual test method.
    • afterTest: Executed after every individual test method.

    The test method

    Use the test method to wrap your test logic. It provides:

    • Isolation: Automatically recreates the UltronTestContext for each test.
    • Assertion Safety: Clears previous softAssertion exceptions at the start of each test.
    • Suppression: You can bypass the common lifecycle hooks using suppressCommonBefore = true and suppressCommonAfter = true within the test function arguments.
    class SampleUltronFlowTest : UltronTest() {
    
        @OptIn(ExperimentalUltronApi::class)
        override val beforeFirstTest = {
            UltronLog.info("Before Class")
        }
    
        override val beforeTest = {
            UltronLog.info("Before test common")
        }
    
        override val afterTest = {
            UltronLog.info("After test common")
        }
    
        @Test
        fun someTest1() = test {
            // Test logic here
        }
    
        @Test
        fun someTest2() = test(
            suppressCommonBefore = true,
            suppressCommonAfter = true
        ) {
            // This test will NOT run beforeTest or afterTest
        }
    }
  11. Compare `runComposeUiTest` and `runUltronUiTest` for Multiplatform

    master

    When testing Compose Multiplatform UI components, you can choose between the standard Compose Testing framework and Ultron.

    • runComposeUiTest: The standard method provided by the Compose Testing framework. It requires using onNode(...) to target elements and provides a SemanticsNodeInteractionProvider context that must be manually passed around if you want to use Page Objects.
    • runUltronUiTest: An Ultron-provided alternative that simplifies interaction syntax by removing the need for onNode(...) wrappers (e.g., using hasTestTag(...).click() directly). It also improves interaction reliability and stability. Crucially, it allows you to call interactions from anywhere, including Page Objects, without manually passing a provider.
    // Standard Compose approach
    runComposeUiTest {
        setContent { /* UI */ }
        onNode(hasTestTag("text")).assertTextEquals("Hello")
    }
    
    // Ultron approach
    runUltronUiTest {
        setContent { /* UI */ }
        hasTestTag("text").assertTextEquals("Hello")
    }
  12. Manage timeouts for assertions and operations

    master

    When using withAssertion, be aware that the total time to failure may increase because Ultron may execute the operation and the assertion block multiple times to verify the failure.

    To prevent tests from hanging, you can control timeouts at two levels:

    1. Assertion Timeout: Use withTimeout(long) inside the assertion block to restrict how long a specific check (like isDisplayed()) should wait.
    2. Operation Timeout: Use withTimeout(long) on the main object to extend the timeout of the primary operation being performed.

    You can combine both to have a long-running operation with a strict, short-lived assertion check.

    // Restrict the assertion check to 3 seconds
    button.withAssertion {
        title.withTimeout(3_000L).isDisplayed()
    }.click()
    
    // Extend the main operation to 10 seconds, but keep the assertion check at 2 seconds
    button.withTimeout(10_000L).withAssertion {
        title.withTimeout(2_000L).isDisplayed()
    }.click()