Ultron UI Testing Framework

website·Indexed 29 days ago

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

A UI testing framework designed to reduce ceremony in test writing. It provides extended APIs for Android and Compose, including support for Espresso, UI Automator, RecyclerView, WebView, and Compose Multiplatform (Alpha). Key features include UltronTest base class, UI blocks, custom assertions, result handlers, and integration with Allure reports.

Tokens
21.5K
Snippets
110
Records
128
Agent score
73%

What's inside Ultron

  1. Overview of Ultron UI Testing Framework

    Ultron is a UI testing framework for Android and Compose Multiplatform built upon Espresso, UI Automator, and Compose UI testing frameworks. It simplifies test development by providing a more expressive syntax, built-in stability mechanisms (automatic retries on exceptions), and support for the Page/Screen Object pattern. Key features include out-of-the-box Allure report generation (Android only), enhanced interaction with RecyclerView and Compose LazyList, and the ability to extend the framework with custom operations.
  2. Interact with Compose Lazy Lists using UltronComposeList

    Ultron provides the UltronComposeList class to wrap interactions with Compose lists (typically LazyColumn or LazyRow). It handles automatic scrolling to items and provides a high-level API for assertions and item retrieval.
    // Basic usage: find a list by test tag and assert it is not empty
    composeList(hasTestTag(contactsListTestTag)).assertNotEmpty()
  3. Choose a UI testing framework for Compose in Ultron

    Ultron supports two primary types of UI tests for Compose applications:

    1. Kotlin Multiplatform UI tests for cross-platform validation.
    2. Platform-specific JUnit-based tests for targeted platform validation (e.g., Android).
  4. Ultron UI testing framework capabilities

    Ultron provides an architecture for UI tests with the following built-in features:

    • Page Objects: Base classes for Pages and Screens to keep locators and user steps stateless.
    • List Handling: Automatic scrolling for RecyclerView and Compose LazyList items using matchers, indices, or positions.
    • Extendable API: Ability to add custom operations as Kotlin extensions using perform and execute to inherit retries and logging.
    • Lifecycle Management: Support for beforeFirstTest, beforeTest, and per-test before/go/after blocks.
    • Step Listeners: Hooks into the lifecycle of every operation for custom logging or screenshot collection.
    • Soft Assertions: Ability to collect multiple assertion failures and report them together rather than stopping at the first failure.
  5. Use Ultron Compose API for UI testing

    The Ultron framework extends the Compose UI testing API, making a wide range of actions and assertions available for SemanticsMatcher objects (created via functions like hasTestTag() or hasText()).
  6. Extend Ultron for Espresso Web

    For Espresso Web, extend the UltronWebElement class. Use executeOperation combined with getUltronWebActionOperation for actions, or getUltronWebAssertionOperation for assertions.
    // Adding a custom action
    fun UltronWebElement.appendText(text: String) = apply {
        executeOperation(
            getUltronWebActionOperation (
                webInteractionBlock = {
                    webInteractionBlock().perform(DriverAtoms.webKeys(text))
                },
                name = "WebElement(${locator.type} = '$value') appendText '$text'",
                description = "WebElement(${locator.type} = '$value') appendText '$text' during $timeoutMs ms"
            )
        )
    }
    
    // Usage
    id("text_input").appendText("some text")
  7. Attach custom artifacts to Allure reports

    To add custom files to an Allure report, use createCacheFile() to generate a temporary file, write your content to it, and then use AttachUtil.attachFile(). The mimeType parameter must be a value from the Ultron MimeType enum.
    fun addMyArtifactToAllure(){
        val tempFile = createCacheFile()
        val result = writeContentToFile(tempFile) // User-implemented method
        val fileName = AttachUtil.attachFile(
            name = "file_name.xml",
            file = tempFile,
            mimeType = MimeType.XML
        )
    }
  8. Define nested UI blocks in Compose Multiplatform

    In Compose Multiplatform, nested UI blocks must be defined using a factory method. This provides an updatedMatcher which ensures the child block is located specifically within the parent block's scope.
    class ProfileBlock(blockMatcher: SemanticsMatcher, blockDescription: String) 
        : UltronComposeUiBlock(blockMatcher, blockDescription) {
        val card = child(
            childMatcher = hasTestTag(contactCardTag),
            uiBlockFactory = { updatedMatcher ->
                ContactCard(
                    blockMatcher = updatedMatcher, 
                    blockDescription = "Contact card '$blockDescription'"
                )
            }
        )
    }
  9. Manage test rule execution order with RuleSequence

    RuleSequence is a replacement for JUnit 4's RuleChain that allows developers to control the order of rule execution. Rules are executed based on the order they are added, categorized into three priority lists: first (executed first), normal (default), and last (executed last). It is recommended to define RuleSequence in a BaseTest class so that subclasses can add their own rules during initialization.
    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 {
            ruleSequence.addLast(activityRule)
        }
    }
  10. Launch an Activity in Ultron Compose tests

    To launch a specific Activity during a Compose test using Ultron, use createUltronComposeRule<A> or createSimpleUltronComposeRule<A>. The createSimpleUltronComposeRule<A> variant utilizes UltronActivityRule for launching and finishing the activity.
    @get:Rule
    val composeTestRule = createUltronComposeRule<YourActivity>()
  11. Extend Ultron with custom ViewActions and ViewAssertions

    To add custom functionality, create a Kotlin extension function for UltronEspressoInteraction<T>. To make the operation fully native (accessible via Matcher<View>, ViewInteraction, and DataInteraction), provide additional extension wrappers.
    // 1. Define the custom ViewAction
    class AppendTextAction(private val value: String) : ViewAction {
        override fun getConstraints() = allOf(isDisplayed(), isAssignableFrom(TextView::class.java))
        override fun perform(uiController: UiController, view: View) {
            (view as TextView).apply { this.text = "${text}$value" }
            uiController.loopMainThreadUntilIdle()
        }
    }
    
    // 2. Extend UltronEspressoInteraction
    fun <T> UltronEspressoInteraction<T>.appendText(text: String) = apply {
        executeAction(
            operationBlock = getInteractionActionBlock(AppendTextAction(text)),
            name = "Append text '$text' to ${getInteractionMatcher()}",
            description = "${interaction!!::class.simpleName} APPEND_TEXT to ${getInteractionMatcher()} during $timeoutMs ms",
        )
    }
    
    // 3. Make it native for all matchers/interactions
    fun Matcher<View>.appendText(text: String) = UltronEspressoInteraction(onView(this)).appendText(text)
    fun ViewInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text)
    fun DataInteraction.appendText(text: String) = UltronEspressoInteraction(this).appendText(text)
    
    // Usage
    withId(R.id.text_input).appendText("some text to append")