

UI tests that simply don't flake
A testing framework for Android and Compose Multiplatform. Espresso, UI Automator and Compose UI testing — with a syntax you actually want to write, and stability built into every action and assertion.
androidTestImplementation("com.atiurin:ultron-compose:2.6.5")
- Views
- Compose
- WebView
- iOS
- Desktop
- Web
- macOS
The same test, minus the ceremony
Ultron keeps the operation names you already know and drops everything else. No new DSL to learn.
- Compose
- LazyList
- Espresso
- RecyclerView
- UI Automator
- WebView
composeTestRule.onNode(hasTestTag("Continue"))
.performClick()
composeTestRule.onNodeWithText("Welcome")
.assertIsDisplayed()
hasTestTag("Continue").click()
hasText("Welcome").assertIsDisplayed()
val itemMatcher = hasText(contact.name)
composeRule
.onNodeWithTag(contactsListTestTag)
.performScrollToNode(itemMatcher)
.onChildren()
.filterToOne(itemMatcher)
.assertTextContains(contact.name)
composeList(hasTestTag(contactsListTestTag))
.item(hasText(contact.name))
.assertTextContains(contact.name)
onView(withId(R.id.send_button))
.check(matches(isDisplayed()))
.perform(click())
withId(R.id.send_button).isDisplayed().click()
onView(withId(R.id.recycler_friends))
.perform(
RecyclerViewActions
.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("Janice")),
click()
)
)
withRecyclerView(R.id.recycler_friends)
.item(hasDescendant(withText("Janice")))
.click()
val device = UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation()
)
device.findObject(
By.res("com.atiurin.sampleapp:id", "button1")
).click()
byResId(R.id.button1).click()
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)))
id("text_input").webKeys(newTitle)
id("button1").webClick()
id("title").hasText(newTitle)
An architecture for UI tests, not just a wrapper
Page Object built in
Page and Screen base classes keep locators and user steps in one stateless place, so tests read like scenarios.
Lists without pain
RecyclerView and Compose LazyList items are addressed by matcher, index or position — scrolling is handled for you.
Extendable API
Add your own operations as Kotlin extensions with perform and execute — they inherit retries, logging and reporting.
Full test lifecycle
beforeFirstTest, beforeTest and per-test before / go / after blocks — set preconditions for a single test without touching the class.
Listeners on every step
Hook into the lifecycle of each operation to log, screenshot or collect anything you need — the same mechanism Allure support uses.
Soft assertions
Collect assertion failures through the test and report them together, instead of stopping at the first one.
Every operation retries itself
Ultron doesn't execute an action once and hope for the best. It repeats the operation until it succeeds or the timeout expires, and only for the exceptions you allow — an unexpected failure still fails fast.
- Execute the action or assertion.
- Analyse the failure — retry only allowed exceptions, abort on the rest.
- Assert the custom result condition, then report the whole step with a readable description.
// waits and retries until the text appears (5s by default)
withId(R.id.result).hasText("Passed")
// a slow screen? ask for more time on this operation only
withId(R.id.result)
.withTimeout(10_000)
.hasText("Passed")
// any operation as a Boolean — no try/catch needed
val isDisplayed = withId(R.id.button).isSuccess {
isDisplayed()
}
Allure report out of the box
Add ultron-allure, apply the recommended config and every operation becomes a report step — with screenshots, view hierarchy and logs attached on failure. No manual instrumentation.
@BeforeClass
@JvmStatic
fun setConfig() {
UltronConfig.applyRecommended()
UltronAllureConfig.applyRecommended()
UltronComposeConfig.applyRecommended()
}
Three libraries, pick what you need
Published to Maven Central. Current version 2.6.5.
dependencies {
androidTestImplementation("com.atiurin:ultron-compose:2.6.5")
androidTestImplementation("com.atiurin:ultron-android:2.6.5")
androidTestImplementation("com.atiurin:ultron-allure:2.6.5")
}