How to write Automated Unit Test and Instrumentation UI Test In Android?
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:
- Unit Test: To test individual functions, classes, and properties in isolation. Tools used: JUnit, Mockito.
- 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.
- UI Test: To test the user interface and user interactions with the app. Tools used: Espresso, UI Automator.
- Performance Test: To evaluate the performance characteristics of the application, such as speed, responsiveness, and resource usage. Tools used: Android Profiler.
- 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:
- What is an Automated Test?
- Why automated tests are important?
- Build-in automated test in Android(Unit Test and Instrumentation Test)
- Unit test in action(We will test the
calculateTip()
function if it gives the correct result in TipTime App). - 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.
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.
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
- 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 oncom.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.
- Right click the
com.appmakerszone.tiptime
directory and then select New > Kotlin Class/File. - 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 oncom.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:
- Create a
composeTestRule
variable set to the result of thecreateComposeRule()
method and annotate it with theRule
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…