Blog Infos
Author
Published
Topics
, ,
Published

This is the second of three articles about Jetpack Compose UI Testing series. As per the previous article, I’d like to begin with a quick introduction into some of the basic aspects, in order to understand the whole context of the topic.

I would like to start introducing and explaining the importance of the semantics API. As I already mentioned in my previous article:

Compose and the View framework have fundamentally different architectures. Views are objects with a defined structure, state, and a clear hierarchy after creation; whereas composable functions do not. Once a composable has emitted its UI there is no possibility for tools to identify a particular component and interact with it. We need a way to describe our compose UI in a structured way in order for other tools to interact with it.

Semantics provide the required structured description of our UI. Every composable can define semantics. Therefore, Compose will generate a Semantics Tree alongside the UI.

With that being said, it is time to review the available tree node finders options in the testing field and analyse each of them from the Clean Code principles point of view.

Don’t mix Production Code and Test Code ⛔

Assuming that most of the developers have understood and applied the Clean Code principles perfectly described by Uncle Bob, there’s no doubt, when it comes to keeping the production code clear of test code, that it is not good practice to add test code when functions, classes, variables and so forth have been defined in production code.

In the official documentation, Testing your Compose Layout, it’s possible to find nodes using “hasTestTag”, which means the inclusion of test code in production code 🚩(RED FLAG).

composeTestRule.onNode(hasTestTag("Players"))
.onChildren()
.filter(hasClickAction())
.assertCountEquals(4)
.onFirst()
.assert(hasText("John"))

Example originally copied from the official Android documentation

 

Use Semantics and Finders 🏅

As mentioned before, Semantics give a meaning to a piece of UI and offer accessibility. Therefore, there is a better way to identify Buttons, Animations, Images, etc. through contentDescription.

@Composable
private fun FilterIcon(modifier: Modifier, onClick: () -> Unit) {
Box(modifier) {
IconButton(
onClick = onClick
) {
Image(
painter = painterResource(R.drawable.ic_filter),
contentDescription = "Filter Button"
)
}
}
}
view raw FilterIcon.kt hosted with ❤ by GitHub

Production Code example

 

In the UI Test, it is as simple as:

private val dialogFilterButton by lazy {
composeTestRule.onNodeWithContentDescription("Filter Button")
}
@Test
fun elementsVisibilityAfterOpeningTheMainScreen() {
setMainContent()
dialogFilterButton.assertIsDisplayed()
}

Test Code example

 

Likewise, using Finders, it’s possible to define some text in an element by text. Then get the reference of that element in your test by using onNodeWithText. This is the most practical strategy, as well as most commonly used, to find elements in Compose which represent buttons.

Robot Pattern in Jetpack Compose 🤖

The Robot Pattern was designed by Jake Wharton at Square back in 2016. The power of this pattern is its ability to create an abstraction layer in order to interact with the UI in a declarative mode. Once created, it is then possible to perform multiple tests in order to verify our use cases without boilerplate code, as well as without maintenance problems related to a refactor.

Some of the big reasons are:

  1. Ease of understanding
    This means we can quickly and easily read and understand what’s being tested without knowing exactly how the test works behind the scenes.
  2. Reuse of code
    By breaking down the tests into steps, each implementation step can be re-used as many times as needed.
  3. Isolating implementation details
    Whichever architecture your app uses, your aim is the single responsibility principle. Sticking to this allows you to switch out an object for a new one, with a new implementation, while still keeping the objects core functionality. This allows for code that is easier to maintain, test, and improve.
The Screen Robot

This class will allocate all of the screen elements and assertion functions:

private val launchesTabItem by lazy {
composeTestRule.onNodeWithText(
"Launches"
)
}
private val launchesListItems by lazy {
composeTestRule.onAllNodesWithContentDescription(
"Item",
substring = true,
useUnmergedTree = true
)
}
private val connectionErrorMessage by lazy {
composeTestRule.onNodeWithText("AN ERROR OCCURRED", useUnmergedTree = true)
}

The screen elements get identified using semantics and/or node finders

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Creating responsive UIs and other nuances of Flutter Web

Flutter for Web is definitely less widespread than Flutter for mobile devices, but in my practice I have found it to be very powerful. Flutter Web has made reusing code between multiple platforms easier than…
Watch Video

Creating responsive UIs and other nuances of Flutter Web

Kon Syrokostas
Software Engineer

Creating responsive UIs and other nuances of Flutter Web

Kon Syrokostas
Software Engineer

Creating responsive UIs and other nuances of Flutter Web

Kon Syrokostas
Software Engineer

Jobs

Let’s look at the functions defined in this class:

fun clickOnLaunchesTab() = launchesTabItem.assertIsDisplayed().performClick()
fun initialElementsShowed() {
launchesTitle.assertIsDisplayed()
dialogFilterButton.assertIsDisplayed()
}
fun listItemsShowed(numItemsShowed: Int) = launchesListItems.assertCountEquals(numItemsShowed)
fun advanceTimeBy(timeToAdvance: Long) = composeTestRule.mainClock.advanceTimeBy(timeToAdvance)
fun errorElementsDisplayed() {
connectionErrorMessage.assertExists().assertIsDisplayed()
connectionErrorAnimation.assertExists().assertIsDisplayed()
}
fun noResultsElementsShowed() {
noResultsText.assertExists().assertIsDisplayed()
listItemsShowed(0)
}

These functions will simplify the test case and will be reused in many cases

 

Comparison

Let’s compare the same UI Test using the Robot pattern and without too.

@Test
fun elementsVisibilityAfterOpeningTheScreen() {
launchesScreenRobot(composeTestRule) {
clickOnLaunchesTab()
initialElementsShowed()
}
}
@Test
fun elementsVisibilityAfterOpeningTheScreen() {
composeTestRule.apply {
onNodeWithText("Launches").performClick()
onNodeWithContentDescription(
"Launches Animation",
useUnmergedTree = true
).assertIsDisplayed()
onNodeWithContentDescription("Filter Button").assertIsDisplayed()
onNodeWithText("LAUNCHES").assertIsDisplayed()
}
}

The first function implements Robot Pattern whereas the second one does not

 

In the previous example, there are not many elements involved; the assertions and interactions are very simple too. However, in a production environment we could potentially encounter a very complex structure which could be transformed into a concise and simplified Robot Pattern version.

DSL

One of the advantages of using the Robot Pattern in the UI Tests is that it’s possible to reference the Screen Robot class as a DSL, where it’s possible to pass the AndroidComposeTestRule as a parameter.
Let’s check how it should be defined:

internal fun launchesScreenRobot(
composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<EntryPointActivity>, EntryPointActivity>,
func: LaunchesScreenRobot.() -> Unit
) = LaunchesScreenRobot(composeTestRule).also { func }
internal open class LaunchesScreenRobot constructor(
private val composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<EntryPointActivity>, EntryPointActivity>
) {
// Robot definitions and functions
}

The result will reflect that shown within the previous UI Test Example. All of the functions can be used inside the DSL, which removes the boiler plate and noise that usually has to be declared.

When NOT to apply Robot Pattern

The aim of this pattern is to abstract & reuse code, make the tests more concise and easy to read. As per other methodologies or alternative frameworks, we need to be pragmatic and ensure that we check whether the result is easier to read, or whether it overcomplicates the test’s definition task.

Full Example🧐

If you would like to check more details and see some of the tests examples I have defined with and without this pattern, make sure you check my Github example SpaceX prepare for Clean Architecture liftoff🚀

References

Official Android Compose Testing

Jake Wharton Blog

Brian Herbst Blog

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu