How to write Automated Unit Test and Instrumentation UI Test In Android?

Animesh Roy
12 min readAug 11, 2024

--

Photo by Mika Baumeister on Unsplash

Testing is a structured method of checking your software to make sure that it works as expected. Before going deep into writing automated unit tests in Android, let us first know briefly what the different categories and types of tests available in Android. Each type of test plays a specific role in ensuring that your Android application is robust, user-friendly, and performs well across different scenarios and devices., Here is the list of primary Andriod Tests:

  1. Unit Test: To test individual functions, classes, and properties in isolation. Tools used: JUnit, Mockito.
  2. Instrumentation Test: To test UI that relies on the Android framework or system, such as Activities, Services, or UI elements. Tools used: AndroidJUnitRunner, Espresso, UI Automator.
  3. UI Test: To test the user interface and user interactions with the app. Tools used: Espresso, UI Automator.
  4. Performance Test: To evaluate the performance characteristics of the application, such as speed, responsiveness, and resource usage. Tools used: Android Profiler.
  5. Compatibility Test: To ensure the app works across different devices, screen sizes, and Android versions. Tools used: Firebase Test Lab, Android Emulator.

In this article, we will focus on automated Unit Tests and Instrumented Tests, also we will work on an app, and implement Automated testing to it. If you want to follow along, Then check out this TipTime App GitHub project.

Table of contents:

  1. What is an Automated Test?
  2. Why automated tests are important?
  3. Build-in automated test in Android(Unit Test and Instrumentation Test)
  4. Unit test in action(We will test the calculateTip() function if it gives the correct result in TipTime App).
  5. Instrumented UI test in action(we test that the app displays the correct tip amount in UI based on the bill amount and tip percentage inputs in TipTime App).

Automated Tests

Automated testing is code that checks to ensure that the piece of code that you wrote works correctly. Testing also provides a way to continuously check the existing code as changes are introduced. Testing is important as part of the app development process. By running tests against your app consistently, one can verify correctness, functional behavior, and usability before releasing it to production or in the hands of public.

Manual testing is always there but in the case of Android, testing can often be automated. Automated testing is an essential part of all software development and Android development is no exception.

As you become familiar with Android development and testing Android apps, you should make it a regular practice to write tests alongside your app code. Creating a test every time you create a new feature in your app reduces your workload later as your app grows. It also provides a convenient way for you to make sure your app works properly without spending too much time manually testing your app.

Why automated tests are important?

In a small simple app, it might seem like we don’t really need tests in our apps, but testing is needed in apps of all sizes and complexities. As our app grows we need to test existing functionality as you add new pieces of code, which is only possible if you have existing tests. When our app grows manual testing takes much more effort as compared to automated testing. Once you start working on apps in production, testing becomes critical when you have a large user base. For example, you must account for many different types of devices running many different versions of Android.

When you run tests before you release new code, you can make changes to the existing code so that you avoid the release of an app with unexpected behaviors.

Remember that automated tests are tests executed through software, as opposed to manual tests, which are carried out by a person who directly interacts with a device. Automated testing and manual testing play a critical role in ensuring that users of your product have a pleasant experience. However, automated tests can be more precise and they optimize your team’s productivity because a person isn’t required to run them and they can be executed much faster than a manual test.

Type of Build-In Automated Tests in Android

Local Unit Tests

Local tests are a type of automated test that directly test a small piece of code to ensure that it functions properly. With local tests, you can test functions, classes, and properties. Local tests are executed on your workstation, which means they run in a development environment without the need for a device or emulator. This is a fancy way to say that local tests run on your computer. They also have very low overhead for computer resources, so they can run fast even with limited resources. Android Studio is ready to run local tests automatically.

Local Unit Test Directory

Instrumentation UI tests

For Android development, an instrumentation test is a UI test. Instrumentation tests let you test parts of an app that depend on the Android API, and its platform APIs and services. Unlike local tests, UI tests launch an app or part of an app, simulate user interactions, and check whether the app reacted appropriately. Throughout this course, UI tests are run on a physical device or emulator. When you run an instrumentation test on Android, the test code is actually built into its own Android Application Package (APK) like a regular Android app.

Instrumentation UI test directory

Now fire up your Android Studio, check out the TipTime App project from GitHub, and Let’s write some local tests.

Unit Tests in Action

Make some changes in MainActivity.kt

We will test the calculateTip function. Local tests directly test methods from the app code, so the methods to be tested must be available to the testing classes and methods. The local test in the following code snippet ensures that the calculateTip() method works correctly, but the calculateTip() method is currently private and thus not accessible from the test. Remove the private designation and make it internal , the line before the calculateTip() method, add the @VisibleForTesting annotation so that it indicates to others that it’s for testing purposes.

@VisibleForTesting
internal fun calculateTip(
amount: Double,
tipPercent: Double = 15.0,
roundUp: Boolean
): String {
var tip = tipPercent / 100 * amount
if (roundUp) {
tip = kotlin.math.ceil(tip)
}
return NumberFormat.getCurrencyInstance().format(tip)
}

Create the test directory

  1. In the project tab, switch Android view to Project view.

2. Right-click on the src > directory > Select New > Directory

3. In the New Directory window, select test/java. (This might be already created for you by Android Studio, if not then create it).

4. The test directory requires a package structure identical to that of the main directory where your app code lives. In other words, just as your app code is written in the main > java > com > appmakerszone > tiptime package, your local tests will be written in test > java > com > appmakerszone > tiptime

Alternatively, If you are using the latest version of Android Studio, you don’t need to do all of these step like switching to project view > creating directory etc. You can simply create a Kotlin class by right clicking on com.appmakerszone.tiptime (test) package.

Create the test class

Now that the test package is ready, it’s time to write some tests! Start by creating the test class.

  1. Right click the com.appmakerszone.tiptime directory and then select New > Kotlin Class/File.
  2. Enter TipCalculatorTests as the class name.

Finally, let us write the test

As previously mentioned, local tests are used to test small pieces of code in the app. The main function of the Tip Time App calculates tips, so there should be a local test that ensures that the tip calculation logic works correctly.

To achieve this, you need to directly call the calculateTip() function like you did in the app code. Then you ensure that the value returned by the function matches an expected value based on the values that you passed to the function.

There are a few things that you should know about writing automated tests. The following list of concepts applies to local and instrumentation tests:

  • Write automated tests in the form of methods.
  • Annotate the method with the @Test annotation. This lets the compiler know that the method is a test method and runs the method accordingly.
  • Ensure that the name clearly describes what the test tests for and what the expected result is.
  • Test methods don’t use logic like regular app methods. They aren’t concerned with how something is implemented. They strictly check an expected output for a given input. That is to say, test methods only execute a set of instructions to assert that an app’s UI or logic functions correctly. You don’t need to understand what this means yet because you see what this looks like later, but remember that test code may look quite different from the app code that you’re used to.
  • Tests typically end with an assertion, which is used to ensure that a given condition is met. Assertions come in the form of a method call that has assert in its name. For example: the assertTrue() assertion is commonly used in Android testing. Assertion statements are used in most tests, but they're rarely used in actual app code.

Write the test

Create a method to test the calculation of a 20% tip for a INR(Indian Rupees) 10 bill amount. The expected result of that calculation is INR 2

package com.appmakerszone.tiptime

import org.junit.Test

class TipCalculatorTests {
@Test
fun calculateTip_20PercentNoRoundup() {

}
}

The calculateTip() method from the MainActivity.kt file in the app code requires three parameters. The bill amount, the tip percent, and a boolean to round the result or not.

fun calculateTip(amount: Double, tipPercent: Double, roundUp: Boolean)

In the calculateTip_20PercentNoRoundup() method, create two constant variables: an amount variable set to a 10.00 value and a tipPercent variable set to a 20.00 value.

val amount = 10.00
val tipPercent = 20.00

The tip amount is formatted based on the locale currency of the device.

Create a expectedTip variable set like:

val expectedTip = NumberFormat.getCurrencyInstance().format(2)

The expectedTip variable is compared to the result of the calculateTip() method later. This is how the test ensures that the method works correctly.

Now, call the calculateTip() method with the amount and tipPercent variables, and pass a false argument for the roundup.

val actualTip = calculateTip(amount = amount, tipPercent = tipPercent, false)

Call this assertEquals() method, and then pass in the expectedTip and actualTip variables as parameters.

Full test function:

package com.appmakerszone.tiptime

import org.junit.Assert.assertEquals
import org.junit.Test
import java.text.NumberFormat

class TipCalculatorTests {

@Test
fun calculateTip_20PercentNoRoundup() {
val amount = 10.00
val tipPercent = 20.00
val expectedTip = NumberFormat.getCurrencyInstance().format(2)
val actualTip = calculateTip(amount = amount, tipPercent = tipPercent, false)
assertEquals(expectedTip, actualTip)
}
}

Note: There are many assertions in the JUnit library. Some common assertions that you might encounter are:

  • assertEquals()
  • assertNotEquals()
  • assertTrue()
  • assertFalse()
  • assertNull()
  • assertNotNull()
  • assertThat()

Run the test

You may have noticed that green arrows appear alongside the line number of your class name and test function. You can click these arrows to run the test. When you click the arrow next to a method, you only run that single test method. If you have multiple test methods in a class, you can click the arrow next to the class to run all the test methods in that class.

Output:

So the bill amount of 10 with tip percentage 20 and without round-up (false). The expected output is 2, that’s why the test passed.

Let’s see what test failure looks like if we change the expected tip to 22

That’s it for the Automated Unit Test. Let’s understand the Instrumentation test now.

Automated Instrumentation Test in Action

In this test we tests that the app displays the correct tip amount based on the bill amount and tip percentage inputs.

The instrumentation directory is created in a similar manner to that of the local test directory.

If you are using the latest version of Android Studio, you don’t need to do all of these step like switching to project view > creating directory etc. You can simply create a Kotlin class by right clicking on com.appmakerszone.tiptime (androidTest) package.

In Android projects, the instrumentation test directory is designated as the androidTest directory.

To create an instrumentation test, you need to repeat the same process that you used to create a local test, but this time create it inside the androidTest directory.

Create a Kotlin class TipUITests

Instrumentation test code is quite different from local test code.

Instrumentation tests test an actual instance of the app and its UI, so the UI content must be set, similar to how the content is set in the onCreate() method of the MainActivity.kt file when you wrote the code for the Tip Time app. You need to do this before you write all instrumentation tests for apps built with Compose.

In the case of the Tip Time app tests, you proceed to write instructions to interact with the UI components so that the tip-calculating process is tested through the UI.

Write test code now:

  1. Create a composeTestRule variable set to the result of the createComposeRule() method and annotate it with the Rule annotation.
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class TipUITests {

@get:Rule
val composeTestRule = createComposeRule()
}

2. Create a calculate_20_percent_tip() method and annotate it with the @Test annotation

package com.appmakerszone.tiptime

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test

class TipUITests {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun calculate_20_percent_tip() {

}
}

3. In the function body, call the composeTestRule.setContent() function. This sets the UI content of the composeTestRule

4. In the function’s lambda body, call the TipTimeTheme() function with a lambda body that calls the TipTimeLayout() function.

import com.example.tiptime.ui.theme.TipTimeTheme

@Test
fun calculate_20_percent_tip() {
composeTestRule.setContent {
TipTimeTheme {
TipTimeLayout()
}
}
}

When you’re done, the code should look similar to the code written to set the content in the onCreate() method in the MainActivity.kt file. Now that the UI content is set up, you can write instructions to interact with the app's UI components. In this app, you need to test that the app displays the correct tip value based on the bill amount and tip percentage inputs.

5. UI components can be accessed as nodes through the composeTestRule. A common way to do this is to access a node that contains a particular text with the onNodeWithText() method. Use the onNodeWithText() method to access the TextField composable for the bill amount:

import androidx.compose.ui.test.onNodeWithText

@Test
fun calculate_20_percent_tip() {
composeTestRule.setContent {
TipTimeTheme {
TipTimeLayout()
}
}
composeTestRule.onNodeWithText("Bill Amount")
}

Next you can call the performTextInput() method and pass in the text that you want entered to fill the TextField composable.

6. Populate the TextField for the bill amount with a 10 value:

import androidx.compose.ui.test.performTextInput

@Test
fun calculate_20_percent_tip() {
composeTestRule.setContent {
TipTimeTheme {
TipTimeLayout()
}
}
composeTestRule.onNodeWithText("Bill Amount")
.performTextInput("10")
}

7. Use the same approach to populate the tip percentageTextField with a 20 value:

@Test
fun calculate_20_percent_tip() {
composeTestRule.setContent {
TipTimeTheme {
TipTimeLayout()
}
}
composeTestRule.onNodeWithText("Bill Amount")
.performTextInput("10")
composeTestRule.onNodeWithText("Tip Percentage").performTextInput("20")
}

After all the TextField composables are populated, the tip displays in a Text composable at the bottom of the screen in the app.

8. In instrumentation tests with Compose, assertions can be called directly on UI components. There are a number of assertions available, but in this case you want to use the assertExists() method. The Text composable that displays the tip amount is expected to display: Tip Amount: INR 2.00.

The currency symbol may change depending on device locale. The usage of ‘$' is just an example. The currency must be formatted based on locale.

9. Make an assertion that a node with that text exists:

package com.appmakerszone.tiptime

import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput
import com.appmakerszone.tiptime.ui.theme.TipTimeTheme
import org.junit.Rule
import org.junit.Test
import java.text.NumberFormat

class TipUITests {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun calculate_20_percent_tip() {

composeTestRule.setContent {
TipTimeTheme {
TipTimeLayout()
}
}
composeTestRule.onNodeWithText("Bill Amount")
.performTextInput("10")
composeTestRule.onNodeWithText("Tip Percentage")
.performTextInput("20")
val expectedTip = NumberFormat.getCurrencyInstance().format(2)
composeTestRule.onNodeWithText("Tip Amount: $expectedTip").assertExists(
"No node with this text was found."
)

}
}

Let us run the test

See our UI Instrumented test passed successfully.

We have written two automated tests in Android. One is a Local Unit Test and another is a UI Instrumented test.

Thanks for reading this article. If you find it helpful, consider giving some clap, also follow me for more article related to Android. Have a nice day…

--

--