Skip to content

open-tool/ultron

Repository files navigation

Ultron

Maven Central Multiplatform CI

Ultron is the simplest framework to develop UI tests for Android & Compose Multiplatform.

It's constructed upon the Espresso, UI Automator and Compose UI testing frameworks. Ultron introduces a range of remarkable new features. Furthermore, Ultron puts you in complete control of your tests!

You don't need to learn any new classes or special syntax. All magic actions and assertions are provided from crunch. Ultron can be easially customised and extended. Wish you exclusively stable tests!

Documentation Releases Telegram

What are the benefits of using the framework?


Documentation

The framework offers an excellent documentation that addresses the majority of significant usage scenarios.

A few words about syntax

The standard syntax provided by Google is intricate and not intuitive. This is especially evident when dealing with LazyList and RecyclerView interactions.

Let's explore some examples:

1. Simple compose operation (refer to the doc here)

Compose framework

composeTestRule.onNode(hasTestTag("Continue")).performClick()
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()

Ultron

hasTestTag("Continue").click()
hasText("Welcome").assertIsDisplayed()

2. Compose list operation (refer to the doc)

Compose framework

val itemMatcher = hasText(contact.name)
composeRule
    .onNodeWithTag(contactsListTestTag)
    .performScrollToNode(itemMatcher)
    .onChildren()
    .filterToOne(itemMatcher)
    .assertTextContains(contact.name)

Ultron

composeList(hasTestTag(contactsListTestTag))
    .item(hasText(contact.name))
    .assertTextContains(contact.name)

3. Simple Espresso assertion and action.

Espresso

onView(withId(R.id.send_button)).check(isDisplayed()).perform(click())

Ultron

withId(R.id.send_button).isDisplayed().click()

This presents a cleaner approach. Ultron's operation names mirror Espresso's, while also providing additional operations.

Refer to the doc for further details.

4. Action on RecyclerView list item

Espresso

onView(withId(R.id.recycler_friends))
    .perform(
        RecyclerViewActions
            .actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("Janice")),
                click()
            )
        )

Ultron

withRecyclerView(R.id.recycler_friends)
    .item(hasDescendant(withText("Janice")))
    .click()

Explore the doc to unveil Ultron's magic with RecyclerView interactions.

5. Espresso WebView operations

Espresso

onWebView()
    .withElement(findElement(Locator.ID, "text_input"))
    .perform(webKeys(newTitle))
    .withElement(findElement(Locator.ID, "button1"))
    .perform(webClick())
    .withElement(findElement(Locator.ID, "title"))
    .check(webMatches(getText(), containsString(newTitle)))

Ultron

id("text_input").webKeys(newTitle)
id("button1").webClick()
id("title").hasText(newTitle)

Refer to the doc for more details.

6. UI Automator operations

UI Automator

val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device
    .findObject(By.res("com.atiurin.sampleapp:id", "button1"))
    .click()

Ultron

byResId(R.id.button1).click() 

Refer to the doc


Acquiring the result of any operation as Boolean value

val isButtonDisplayed = withId(R.id.button).isSuccess { isDisplayed() }
if (isButtonDisplayed) {
    //do some reasonable actions
}

Why are all Ultron actions and assertions more stable?

The framework captures a list of specified exceptions and attempts to repeat the operation during a timeout period (default is 5 seconds). Of course, you have the ability to customize the list of handled exceptions. You can also set a custom timeout for any operation.

withId(R.id.result).withTimeout(10_000).hasText("Passed")

3 steps to develop a test using Ultron

We advocate for a proper test framework architecture, division of responsibilities between layers, and other best practices. Therefore, when using Ultron, we recommend the following approach:

  1. Create a Page Object and specify screen UI elements as Matcher<View> objects.
object ChatPage : Page<ChatPage>() {
    private val messagesList = withId(R.id.messages_list)
    private val clearHistoryBtn = withText("Clear history")
    private val inputMessageText = withId(R.id.message_input_text)
    private val sendMessageBtn = withId(R.id.send_button)
}

It's recommended to make all Page Objects as object and descendants of Page class. This allows for the utilization of convenient Kotlin features. It also helps you to keep Page Objects stateless.

  1. Describe user step methods in Page Object.
object ChatPage : Page<ChatPage>() {
    fun sendMessage(text: String) = apply {
        inputMessageText.typeText(text)
        sendMessageBtn.click()
        getMessageListItem(text).text
             .isDisplayed()
             .hasText(text)
    }

    fun clearHistory() = apply {
        openContextualActionModeOverflowMenu()
        clearHistoryBtn.click()
    }
}

Refer to the full code sample ChatPage.class

  1. Call user steps in test
    @Test
    fun friendsItemCheck(){
        FriendsListPage {
            assertName("Janice")
            assertStatus("Janice","Oh. My. God")
        }
    }
    @Test
    fun sendMessage(){
        FriendsListPage.openChat("Janice")
        ChatPage {
            clearHistory()
            sendMessage("test message")
        }
    }

Refer to the full code sample DemoEspressoTest.class

In essence, your project's architecture will look like this:

acrchitecture


Allure report

Ultron has built in support to generate artifacts for Allure reports. Just apply the recommended configuration and set testIntrumentationRunner.

For the complete guide, refer to the Allure description

@BeforeClass @JvmStatic
fun setConfig() {
    UltronConfig.applyRecommended()
    UltronAllureConfig.applyRecommended()
    UltronComposeConfig.applyRecommended() 
}

allure

allure compose

Add Ultron to your project

Gradle

repositories {
    mavenCentral()
}

dependencies {
    androidTestImplementation 'com.atiurin:ultron-android:<latest_version>'
    androidTestImplementation 'com.atiurin:ultron-allure:<latest_version>'
    androidTestImplementation 'com.atiurin:ultron-compose:<latest_version>'
}

Please, read gradle dependencies management doc.