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!
- Exceptional support for Compose
- Out-of-the-box generation of Allure report
- A straightforward and expressive syntax
- Ensured Stability for all actions and assertions
- Complete control over every action and assertion
- Incredible interaction with RecyclerView and Compose lists.
- An Architectural approach to developing UI tests
- An incredible mechanism for setups and teardowns (You can even set up preconditions for a single test within a test class, without affecting the others)
- The ability to effortlessly extend the framework with your own operations
- Accelerated UI Automator operations
- Ability to monitor each stage of operation execution with Listeners
- Custom operation assertions
The framework offers an excellent wiki that addresses the majority of significant usage scenarios.
The standard syntax provided by Google is intricate and not intuitive. This is especially evident when dealing with RecyclerView interactions.
Let's explore some examples:
1. Simple compose operation (refer to the wiki 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 wiki here)
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)
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 wiki for further details.
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 wiki to unveil Ultron's magic with RecyclerView interactions.
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 wiki for more details.
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 wiki
val isButtonDisplayed = withId(R.id.button).isSuccess { isDisplayed() }
if (isButtonDisplayed) {
//do some reasonable actions
}
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")
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:
- 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.
- 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
- 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:
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 wiki
@BeforeClass @JvmStatic
fun setConfig() {
UltronConfig.applyRecommended()
UltronAllureConfig.applyRecommended()
UltronComposeConfig.applyRecommended()
}
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 wiki page.
It is required to use AndroidX libraries. You can get some problems with Android Support ones.