Blog Infos
Author
Published
Topics
, ,
Published

How can we approach UI development in Android while ensuring we are writing high-quality software from the design spec? Test Driven Development (TDD) is a way of approaching software development that is very handy for both feature writing and bug fixing.

It provides a path for developers to request more information when needed before writing “just in case” software, also known as YAGNI (You Aren’t Gonna Need It), while fostering the creation of optimized code using the KISS principle (Keep It Simple, Stupid), I guess? Additionally, TDD is highly effective for writing code that is easy to update. Introducing a new test is straightforward once the suite is running, and tests are always there for you if you accidentally or intentionally decide to update the behavior of your software.

So, if we all agree that TDD is the way, let’s give it a try and attempt writing our UI with it using Jetpack Compose.

Plan

First, let’s consider a common scenario for a mobile engineer: receiving a new design specification for a brand new screen to be added to our app. For this exercise, I have chosen Taras Migulko’s fantastic work on an E-commerce app design from Dribbble.

Specifically, we will be implementing one of the UI elements of a clothing list of items, as depicted in the following screen:

Keeping this in mind, let’s set up our development environment. In this case, we will start a new project with Compose, assuming that this step has already been completed for the purpose of this sample.

I’ve created a default project using the templates of Android Studio for an Activity + Compose. Let’s make sure our project includes all the required libraries to do TDD in Compose:

androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
view raw build.gradle hosted with ❤ by GitHub

Lastly, if we choose to follow a Test-Driven Development (TDD) approach, it is essential to remember and adhere to the three rules of TDD defined by Robert C. Martin, also known as Uncle Bob. These rules serve as our development traffic lights🚦when writing or updating any tests in the app:

1. Write production code only to pass a failing unit test.

2. Write no more of a unit test than sufficient to fail (compilation failures are failures).

3. Write no more production code than necessary to pass the one failing unit test.

By following these rules, we ensure that our development process remains focused and efficient.

🚦Action

The Design Spec is ready and the environment is set, so How do we approach a new piece of UI with a test?

This will depend on each framework used for UI, for this example we’ll be using Jetpack Compose so it becomes relevant to understand how to “Think in Compose

With Compose is easy to dissect the UI into very small pieces that are described as Composables that will be put together to render the full UI.

So the first thing we’ll need to do Is have a close look into the UI to define how can we divide it and go from small to big.

Taking a first glance I’ve identified (at least) five different Composables that will conform to this screen:

  • A top Action Bar which includes the Back, Name, Item Count and Filter action.
  • A main List of clothing items
  • Each Clothing Item
  • The Clothing Item Content (display name, price and the add to cart action button)
  • The Favourite Button

There’s already some added value in doing this as now we are starting to surface both reusable components such as the top action bar and also starting to think about naming for our composables, without even touching a single line of code.

So let’s go ahead and pick the smallest possible composable from our new screen; the Favourite Button.

Assuming this behaves as any favourite button this will have to support 2 states; filled heart and empty heart. Let’s write our first test:

@Test
fun whenFavouriteIsTrueThenFilledHeartIconIsDisplayed() {}
view raw FirstTest.kt hosted with ❤ by GitHub

Worth mentioning at this stage that we’ll be following the suggested documentation on Testing Compose so we also need to add the composeTestRule and initialise our screen using its setContent function:

class ClothingScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun whenFavouriteIsTrueThenFilledHeartIconIsDisplayed() {
composeTestRule.setContent {
ClothingAppTDDTheme {
// Our composable to test here
}
}
}
}

Let’s now bring in our test condition

@Test
fun whenFavouriteIsTrueThenFilledHeartIconIsDisplayed() {
val isFavourite = true
....
view raw firstTest.kt hosted with ❤ by GitHub

🔴 And when we write our Composable name (that still does not exist) we brake the second TDD rule; the test no longer compiles

To address this issue we create a new Kotlin file and write our Composable

@Composable
fun FavouriteButton(isFavourite: Boolean) {
}

And we go back to the test

🟢 Compilation issue addressed! We may continue writing the test now

@Test
fun whenFavouriteIsTrueThenFilledHeartIconIsDisplayed() {
val isFavourite = true
composeTestRule.setContent {
ClothingAppTDDTheme {
FavouriteButton(isFavourite)
}
}
composeTestRule.onNodeWithContentDescription("Filled Heart Icon").assertIsDisplayed()
}
view raw FirstTest.kt hosted with ❤ by GitHub

So the test is ready and compiles, next step; let’s run it

🔴 We made it; we have a failing test now. Let’s follow TDD rules 1 and 2 to make it pass before writing any new tests.

@Composable
fun FavouriteButton(isFavourite: Boolean) {
Icon(imageVector = Icons.Rounded.Favorite, contentDescription = "Filled Heart Icon")
}

And I know what you are thinking, we are not checking the boolean! We are not supporting the other scenarios, we could write so much more code! But this, this is the exact point where you need to take that hat of and pick the TDD hat If you want to make the switch and only think: Are we following all the TDD rules? Yes, then it’s ok, let’s keep going.

🟢 Test is going fully green now, this means we are able to write the next test. So let’s try the other scenario our composable needs to support, displaying an empty heart if isFavourite is false.

@Test
fun whenFavouriteIsFalseThenEmptyHeartIconIsDisplayed() {
val isFavourite = false
composeTestRule.setContent {
ClothingAppTDDTheme {
FavouriteButton(isFavourite)
}
}
composeTestRule.onNodeWithContentDescription("Empty Heart Icon").assertIsDisplayed()
}
view raw SecondTest.kt hosted with ❤ by GitHub

🔴 And when we run it:

So once again, following TDD rule number 1 & 3 we go back to our composable and add the required logic to make this test pass, being careful enough that our previous test will work too this time.

@Composable
fun FavouriteButton(isFavourite: Boolean) {
if (isFavourite) {
Icon(imageVector = Icons.Rounded.Favorite, contentDescription = "Filled Heart Icon")
} else {
Icon(imageVector = Icons.Rounded.FavoriteBorder, contentDescription = "Empty Heart Icon")
}
}

And we make sure to run the entire suite now so that both tests are run:

🟢 And we can see all our tests going green now!

Finally we can update our tests to make sure that the other icon is not displayed in the incorrect scenario.

class ClothingScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun whenFavouriteIsTrueThenFilledHeartIconIsDisplayed() {
val isFavourite = true
composeTestRule.setContent {
ClothingAppTDDTheme {
FavouriteButton(isFavourite)
}
}
composeTestRule.onNodeWithContentDescription("Filled Heart Icon").assertIsDisplayed()
composeTestRule.onNodeWithContentDescription("Empty Heart Icon").assertDoesNotExist()
}
@Test
fun whenFavouriteIsFalseThenEmptyHeartIconIsDisplayed() {
val isFavourite = false
composeTestRule.setContent {
ClothingAppTDDTheme {
FavouriteButton(isFavourite)
}
}
composeTestRule.onNodeWithContentDescription("Empty Heart Icon").assertIsDisplayed()
composeTestRule.onNodeWithContentDescription("Filled Heart Icon").assertDoesNotExist()
}
}

With all of this in place we now have peace of mind that our Composable will do exactly what we need to, so we can take a pause on our Test writing and go to the Composable itself to make sure It looks as it should by adding Its background, colors and right sizing.

@Composable
@Preview
fun FavouriteButton(isFavourite: Boolean = true) {
Box(contentAlignment = Alignment.Center) {
Canvas(modifier = Modifier.size(35.dp), onDraw = {
drawCircle(color = Color.White)
})
if (isFavourite) {
Icon(
imageVector = Icons.Rounded.Favorite,
contentDescription = "Filled Heart Icon"
)
} else {
Icon(
imageVector = Icons.Rounded.FavoriteBorder,
contentDescription = "Empty Heart Icon"
)
}
}
}

Conclusion

As you can see with this process of following just the 3 rules of TDD, stopping when required 🔴 and moving on when we can 🟢 we can ensure that our UI will work as expected and is fully covered in case we want to update it in the future; a.k.a.: high-quality software.

By incorporating Test-Driven Development (TDD) into our development process, we eliminate the guesswork associated with coding and rely on trial and error to verify functionality. Instead, our code is designed to precisely meet our requirements. TDD acts as a lighthouse, guiding us to write the necessary code while helping us identify any gaps in the requirements specification through thoughtful consideration.

Moreover, implementing TDD in Jetpack Compose is a straightforward process, as we have seen. This approach resolves potential issues during the design phase, allowing us to confidently write our user interface (UI) knowing that it will seamlessly integrate with the business logic layers.

In upcoming posts I’ll continue developing this entire screen with TDD for now I encourage you to embrace TDD from the outset rather than writing tests after developing the code. By doing so, you will write less code, think through your implementation thoroughly, and easily identify any missing components in the requirements. Embrace the power of TDD and have a wonderful day!

This article was previously published on proandroiddev.com

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

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

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu