UI Testing with Jetpack Compose: Your App’s Sidekick Against Bugs
Hey there, As programmers, We know that great code comes with great responsibility, and that’s why UI testing is such an important tool in our arsenal. UI testing is like a secret weapon that allows us to make sure our app looks and works perfectly for our users. It’s like having a superpower, except we don’t need to be bitten by a radioactive spider to get it.
In this article, I’m going to show you how to unlock the power of UI testing with Jetpack Compose. You don’t need to be a coding guru or have any special skills or experience. All you need is an eagerness to learn and a willingness to take on new challenges.
To get started, we’ll be using a sample calculator app that you can download from GitHub HERE. Don’t worry, you don’t need to be a math genius to use it. It’s a simple app that performs basic mathematical operations, and it’ll be the perfect playground for our UI testing shenanigans.
But before we get to the fun part, we need to set up the testing environment. Don’t worry, it’s a breeze, and I’ll guide you through it step by step. So let’s get started by adding these dependencies to our app module’s build.gradle file:
// Test rules and transitive dependencies:
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
// Needed for createAndroidComposeRule, but not createComposeRule:
debugImplementation 'androidx.compose.ui:ui-test-manifest'
Now, we need to create a new class called “CalculatorOperationsE2ETest” in the “androidTest” package of our project. This is where we’ll put our UI tests.
To create the new class in the “Project” view, right-click on the “androidTest” package, and select “New” -> “Kotlin File/Class”. Name the new class “CalculatorOperationsE2ETest” and make sure it’s located in the “androidTest” package.
Now we’re ready to start writing our UI tests. First, we’ll write separate tests for each of the calculator’s four basic operations: addition, subtraction, multiplication, and division. But that’s not all, we’ll also write an end-to-end test that combines all four operations into one seamless experience.
Let’s see the imports we’ll be needing for our UI test class:
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
The androidx.compose.ui.test
package provides functions to interact with the UI components, while createAndroidComposeRule
is a utility function for creating the test rule to launch our activity. AndroidJUnit4
is the test runner, and Assert
is used for making assertions about our tests. Rule
is a JUnit annotation that allows us to define a test rule, and Test
is another JUnit annotation that identifies a method as a test.
Next, we define the test class and annotate it with the @RunWith(AndroidJUnit4::class)
annotation, which tells JUnit to use the AndroidJUnit4 test runner.
@RunWith(AndroidJUnit4::class)
class CalculatorOperationsE2ETest {
...
}
We also define a test rule using the createAndroidComposeRule
function, which launches our MainActivity
.
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
We then define constants for the calculator buttons using the hasText()
and hasClickAction()
functions from the androidx.compose.ui.test
package. These will help us locate the corresponding buttons in our app so that we can interact with them during our test.
private val oneButton = hasText("1") and hasClickAction()
private val twoButton = hasText("2") and hasClickAction()
private val threeButton = hasText("3") and hasClickAction()
private val fourButton = hasText("4") and hasClickAction()
private val additionButton = hasText("+") and hasClickAction()
private val subtractionButton = hasText("-") and hasClickAction()
private val multiplicationButton = hasText("x") and hasClickAction()
private val divisionButton = hasText("/") and hasClickAction()
private val equalsButton = hasText("=") and hasClickAction()
Alrighty, time to get our hands dirty with some operation tests! First, let’s start with our addition operation test. We’re going to perform some button clicks and make sure the calculator displays the correct answer, just like a math genius would.
@Test
fun additionOperationTest() {
composeTestRule.onNode(oneButton).performClick()
composeTestRule.onNode(additionButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("1+2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("3.0").assertIsDisplayed()
}
In this test, we start by clicking on the buttons for 1, +, and 2. Next, we assert that the displayed text is ‘1+2’. Now that we’re sure we’ve got the right inputs, we’re going to click the equals button and assert that the displayed text is ‘3.0’. Try running the test.
Note: if facing issues with Android 13 emulator, run the test on Android 12 or lower api level emulator.
Voila! We just tested the addition operation like a pro.
But we’re not done yet! We’ve got more operations to test. Next up, subtraction. We’re going to do the same thing here. Click the buttons for 1, -, and 2, and assert that the displayed text is ‘1–2’. Then click equals and make sure the calculator displays ‘-1.0’. It’s like we’re doing magic tricks with code!
@Test
fun subtractionOperationTest() {
composeTestRule.onNode(oneButton).performClick()
composeTestRule.onNode(subtractionButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("1-2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("-1.0").assertIsDisplayed()
}
But wait, there’s more! Let’s not forget about multiplication and division. We’ll perform our button clicks, make our assertions, and watch the calculator display the correct answer, just like that.
@Test
fun multiplicationOperationTest() {
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNode(multiplicationButton).performClick()
composeTestRule.onNode(fourButton).performClick()
composeTestRule.onNodeWithText("2x4").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("8.0").assertIsDisplayed()
}
@Test
fun divisionOperationTest() {
composeTestRule.onNode(fourButton).performClick()
composeTestRule.onNode(divisionButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("4/2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("2.0").assertIsDisplayed()
}
And for the grand finale, we’ll write our end-to-end test that combines all of these operations together. We’ll perform the button clicks for each operation, make our assertions, and finally assert the total result.
@Test
fun operationsE2ETest() {
composeTestRule.onNode(threeButton).performClick()
composeTestRule.onNode(additionButton).performClick()
composeTestRule.onNode(fourButton).performClick()
composeTestRule.onNodeWithText("3+4").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("7.0").assertIsDisplayed()
composeTestRule.onNode(subtractionButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("7.0-2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("5.0").assertIsDisplayed()
composeTestRule.onNode(multiplicationButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("5.0x2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("10.0").assertIsDisplayed()
composeTestRule.onNode(divisionButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("10.0/2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("5.0").assertIsDisplayed()
}
Here is the complete test class code:
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
@RunWith(AndroidJUnit4::class)
class CalculatorOperationsE2ETest {
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
private val oneButton = hasText("1") and hasClickAction()
private val twoButton = hasText("2") and hasClickAction()
private val threeButton = hasText("3") and hasClickAction()
private val fourButton = hasText("4") and hasClickAction()
private val additionButton = hasText("+") and hasClickAction()
private val subtractionButton = hasText("-") and hasClickAction()
private val multiplicationButton = hasText("x") and hasClickAction()
private val divisionButton = hasText("/") and hasClickAction()
private val equalsButton = hasText("=") and hasClickAction()
@Test
fun additionOperationTest() {
composeTestRule.onNode(oneButton).performClick()
composeTestRule.onNode(additionButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("1+2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("3.0").assertIsDisplayed()
}
@Test
fun subtractionOperationTest() {
composeTestRule.onNode(oneButton).performClick()
composeTestRule.onNode(subtractionButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("1-2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("-1.0").assertIsDisplayed()
}
@Test
fun multiplicationOperationTest() {
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNode(multiplicationButton).performClick()
composeTestRule.onNode(fourButton).performClick()
composeTestRule.onNodeWithText("2x4").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("8.0").assertIsDisplayed()
}
@Test
fun divisionOperationTest() {
composeTestRule.onNode(fourButton).performClick()
composeTestRule.onNode(divisionButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("4/2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("2.0").assertIsDisplayed()
}
@Test
fun operationsE2ETest() {
composeTestRule.onNode(threeButton).performClick()
composeTestRule.onNode(additionButton).performClick()
composeTestRule.onNode(fourButton).performClick()
composeTestRule.onNodeWithText("3+4").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("7.0").assertIsDisplayed()
composeTestRule.onNode(subtractionButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("7.0-2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("5.0").assertIsDisplayed()
composeTestRule.onNode(multiplicationButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("5.0x2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("10.0").assertIsDisplayed()
composeTestRule.onNode(divisionButton).performClick()
composeTestRule.onNode(twoButton).performClick()
composeTestRule.onNodeWithText("10.0/2").assertIsDisplayed()
composeTestRule.onNode(equalsButton).performClick()
composeTestRule.onNodeWithText("5.0").assertIsDisplayed()
}
}
here’s an example of how the output might look like when running the test file in the emulator:
In conclusion, UI testing is a critical aspect of app development, and Jetpack Compose offers an easy and enjoyable way to write UI tests. By following the steps outlined in this article, you can elevate your UI testing skills and ensure your app is the best it can be for your users.