Crafting Effective Instrumentation Tests: A Step-by-Step Tutorial with Kaspresso

Freddy Domínguez
11 min readMay 9, 2023

--

A comprehensive guide for being an Testing Automator Engineer

Source: https://proandroiddev.com/kaspresso-the-autotest-framework-that-you-have-been-looking-forward-to-part-i-e102ed384d11

Nowadays, we live in a world where Testing and Automation are constantly evolving. As a result, we need to adapt quickly to the latest tools and trends. In this article/blog, I will guide you through implementing an Instrumental testing framework in Android environments that provides an efficient and scalable approach to writing UI test-cases for Android Compose views to make your test more readable, robust, and maintainable to ensure long-term effectiveness.

The blog is divided in 10 brief sections:

  1. The Importance of instrumental testing
  2. Frameworks and tools
  3. Set up Kaspresso dependencies
  4. App IMDB Compose Views
  5. Set up Instrumental Tests
  6. Writing our first test-case
  7. Assertions and Matchers
  8. Build your custom Perform
  9. Running our test-cases
  10. Common issues

Section 1: The Importance of instrumental testing

As we know, in our daily work as developers, we are constantly writing code to build new features and fix bugs or just simply update the assets. However, we can’t be sure that our code is working as expected until we test it. There are two types of tests we can use to test our code immediately: unit tests and instrumental tests. The former to test each individual component or artifact, and the latter to test the perspective of users.

Why do we need instrumental tests? Do we really need them?

The answer is yes. We need instrumental tests just because they are the best way to test our application in a real environment such as mobile devices, tablets, kindles, OTT: Android TV FirebaseTV, and so on.

Unit tests are great for testing small pieces of code that could be isolated from the rest of the application. Nonetheless, they are not able to test the entire application.

For example: if we want to test the behavior of a button, we can write a unit test for it. Nevertheless, if we want to test the behavior of the entire screen, we will write an instrumental test-case.

In my experience working with OTT platforms, I have been finding for years that instrumental tests are the only way to verify carefully that acceptance criteria is being met as colors, timings, views positions, scrolling and delaying.

Tips: Use flavors for each device in case use multiple devices and want to test them correctly

Section 2: Frameworks and tools

There are several frameworks and tools that you can use to write instrumental tests for your Android application. In this section, I will introduce some of the most popular frameworks and tools that you can use to write instrumental tests for your Android application:

a. Espresso

Espresso is a testing framework that allows you to write concise, beautiful, and reliable Android UI tests. Espresso is a part of the AndroidX test library. Nevertheless, it has some limitations. For instance, it doesn’t support testing of Compose UI components.

b. UI Automator

UI Automator is a framework for writing UI tests for Android. It is a part of the Android SDK. UI Automator is a great tool for testing Android applications. It’s not compatible with some older versions of Android.

c. Robolectric

Roboletric is a framework that brings fast and reliable unit tests to Android. Tests run inside the JVM on your workstation in seconds.It also provides a simulated environment for UI interactions.

d. Appium

Appium is an open-source test automation framework for mobile apps that supports multiple programming languages and platforms. It uses WebDriver protocol to interact with UI elements and also supports native and hybrid apps like Flutter.

e. Kaspresso

Kaspresso is a relatively new testing framework that builds on top of Espresso and adds additional functionality and features to keep up-to-date. Kaspresso aims to simplify and streamline the process of writing and maintaining instrumental tests, providing a more efficient and scalable way to write UI tests for Android. With Kaspresso, you can write tests that are more readable, robust, and maintainable. That’s why I choose to use Kaspresso for my tests. In fact, I am writing this article to teach you how to use it simply and efficiently.

Section 3: Set up Kaspresso dependencies

Once you have your application working, you can start writing instrumental tests for it. In this section, I will show you how to set up Kaspresso with Gradle dependencies.

Starting with Kaspresso, you should add the following dependencies to your build.gradle file or isolate configuration in a separate file and apply it to your build.gradle file.

// grafle dependencies file for the project
ext.testComposeImp = {
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.1.0"
androidTestImplementation('org.hamcrest:hamcrest:2.2')
androidTestImplementation 'com.kaspersky.android-components:kaspresso:1.5.1'
// this dependency is required for using Compose in tests (mandatory)
androidTestImplementation "com.kaspersky.android-components:kaspresso-compose-support:1.5.1"
}

Then add the testComposeImp extension to your build.gradle file in your app module by using the following code snippet:

// use 'testComposeImp' from '../extensions.gradle' in my app module
apply from: '../extensions.gradle'
dependencies {
with testComposeImp
}

The with keyword helps us to apply the extension block to the dependencies block. That means that we can implement the entire list of dependencies in our application just by attaching it.

Alert: Do not forget to sync your project after adding them.

Section 4: App IMDB Compose Views

A brief look at my IMDB Compose application

Previous to starting writing your awesome instrumental tests, I give you a quick glance at what my IMDB Compose application does:

Source: https://github.com/romellfudi/imdbkata/tree/instrumental_test/snapshots
  • Ensure valid emails and passwords with the app registration screen.
  • Display only popular and top-rated movies.
  • Show backdrops, posters and details when selecting a movie.
  • Display cast members and recommendations for movies selected.
  • Allow users to add movies to their favorites with a yellow badge.
  • Easily find desired movies with the search feature.
  • Display users’ photo and name on the profile page.
  • End the session via the logout button or device back button.

Section 5: Set up Instrumental Tests

Now, let’s start writing tests. First, you need to create a new package in your app module called androidTest. Subsequently, create a new package inside the androidTest package with the same package name as your app. Our instrumental suit testing cases will be called DojoInstrumentalTest.kt. You can name it whatever you want, but I recommend using the same name as your app by adding InstrumentalTest at the end to identify it quickly.

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.kaspresso.Kaspresso

@RunWith(AndroidJUnit4::class)
class DojoInstrumentalTest : TestCase(
kaspressoBuilder = Kaspresso.Builder.withComposeSupport()
) {
...
}

In the above code, we ought to extend our class from TestCase, which is a base class for test-cases. DojoInstrumentalTest class will contain all of our testing cases. Next, we are using Kaspresso.Builder.withComposeSupport() method to create a new Kaspresso instance with Compose support. At last, we will use the @RunWith annotation to tell JUnit to run our tests with AndroidJUnit4.

Kaspresso.Builder.withComposeSupport() // is a factory method with Compose support.

For one and all test-cases, we are required to add @Test annotation, this annotation tells our framework that this method is a test-case. We must also add the run keyword before the test-case name. This will allow us to run our test-cases with Kaspresso. The run function is a protected function from BaseTestCase, which is a base class for TestCase.

@Test
fun aSimpleTestCaseWorkflow() = run {
... // test-case code body
}

To tell Kaspresso that we want to run a specific app, we need to use a testRule to identify the first activity that will be launched. In this case, we will use the MainActivity. In our scenario, we need to use the createAndroidComposeRule function to use Compose. The @get:Rule annotation tells JUnit that the value is retrieved from the method.

@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()

Section 6: Writing our first test-case

On the occasion that you need granting permissions, you can use the grantPermissionRule function to grant permissions to your app. This is really helpful when you need extra permissions namely call phone, accessibility service, ACTION_MANAGE_OVERLAY_PERMISSION, and other ones. In the IMDB Compose app, we just got to grant internet permission to our app.

@get:Rule
val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.INTERNET,
...
)

Section 7: Assertions and Matchers

Now, we are ready to write our first test-case. In this case, we will test the register feature. So we need to do two important things: 1) Tag the Compose Views by using Compose testing UI Identifiers and 2) Link our Compose testing UI Identifiers to our UI elements.

1) Tag the Compose Views by using testing Identifies:

@Composable
fun MyView() {
View(
modifier = Modifier
.semantics [ testTag = "my_view" ] // add testing tag identify
) {
// ...
}
}

In the previous code, we are using the semantics modifier to tag our view with the testTag property. This will allow us to identify our view in our test-cases.

2) Link our Compose testing UI Identifies to our UI elements:

class ComposeMainActivity(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ComposeMainActivity>(
semanticsProvider = semanticsProvider,
) {
// links the tags with selectors
// UI elements < —------ > Compose UI testing Identifies
val imdbLogoImage: KNode = child { hasTestTag("splash_screen_image") }
val emailFieldText: KNode = child { hasTestTag("login_screen_email") }
val passwordFieldText: KNode = child { hasTestTag("login_screen_password") }
val loginButton: KNode = child { hasTestTag("login_screen_login_button") }
val newRegisterButton: KNode = child { hasTestTag("login_screen_new_register_button") }
val guestAction: KNode = child { hasTestTag("login_screen_guest_action") }
val nameRFieldText: KNode = child { hasTestTag("register_screen_name") }
val emailRFieldText: KNode = child { hasTestTag("register_screen_email") }
val passwordRFieldText: KNode = child { hasTestTag("register_screen_password") }
val registerButton: KNode = child { hasTestTag("register_screen_register_button") }
}

To use a semantics modifier to tag them, we need a class which inherits from ComposeScreen and takes a SemanticsNodeInteractionsProvider as a parameter. This class ComposeMainActivity is parameterized itself to enable method chaining. The UI elements are represented by KNode objects(ComposeScreen's children).

Consequently, we tell the run block what steps we want to execute. In this case, we want to open the Splash screen and check if the IMDB logo is displayed. Inside the step block, we tell the Kaspresso to “open the Splash screen”, soon after we use the onComposeScreen function to tell Kaspresso we want to interact with the ComposeMainActivity class. Finally, we use the assertIsDisplayed function to check if the IMDB logo is displayed.

@Test
fun aSimpleTestCaseWorkflow() = run {
step("Open Splash screen") {
onComposeScreen<ComposeMainActivity>(composeTestRule) {
// verify the assertion
imdbLogoImage { assertIsDisplayed() }
}
}
}

Like other testing frameworks, Kaspresso has a lot of functions to interact with UI elements. In this test-case, we are using the assertIsDisplayed function to check if the IMDB logo Image (UI element) is displayed. This function is used to check if the UI element is displayed on the screen. If the element is not displayed, it will be reported in the output means the whole test will fail.

Note: The name of the steps is optional, but it is good practice to include them. This will help everyone to identify and understand what each step does, and it will make it easier to debug them

Section 7: Assertions and Matchers

There are considerable assertion functions available to verify the state of the UI elements. I will present some of them to you:

    step("Verify if everything is working as expected") {
onComposeScreen<ComposeMainActivity>(composeTestRule) {
newRegisterButton {
assertIsDisplayed() // check if the UI element is displayed
assertIsNotDisplayed() // check if the UI element is not displayed
assertTextEquals("New Register") // check if the UI element has the same text
assertTextContains("Register") // check if the UI element contains the text
performClick() // click on the UI element
assertHasClickAction() // check if the UI element has a click action
assertIsEnabled() // check if the UI element is enabled
}
toggleButton {
assertIsToggleable() // check if the UI element is toggleable
}
emailRFieldText {
assertIsDisplayed()
performTextInput("") // type text on the UI element
}
}
}
...

Section 8: Build your custom Perform

Just In case you really need a custom action to perform as usually done in your activities of your applications, Fragment, or Compose screen, you can create your own perform function. Let me demonstrate how to do it:

...
// a perform action to go back to the previous screen or simulate the back button
fun performBackAction() {
composeTestRule.activityRule.scenario.onActivity { activity ->
activity.onBackPressedDispatcher.onBackPressed()
}
}
...
step("Navigate among recyclerView and movie details, then logout and finally close app") {
onComposeScreen<ComposeMainActivity>(composeTestRule) {
...
// access to details
movieView1 { performClick() }
// exit from details
performBackAction()
// access to details
movieView2 { performClick() }
// exit from details
performBackAction()
// access to details
movieView3 { performClick() }
// exit from details
performBackAction()
// logout
performBackAction()
// close app
performBackAction()
}
}
...

The code previously mentioned, we created a method which is called performBackAction to simulate the pressing button action.

Another example is simply closing the keyboard of devices to avoid any potential problems:

...
// a perform action to close soft keyboard
fun performCloseSoftKeyBoard() {
composeTestRule.activityRule.scenario.onActivity { activity ->
val inputMethodManager =
activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(activity.currentFocus?.windowToken, 0)
}
}
...
step("Register new user") {
onComposeScreen<ComposeMainActivity>(composeTestRule) {
newRegisterButton { // register button from login screen
assertIsDisplayed()
assertTextEquals(getResourceString(R.string.to_register))
performClick()
}
nameRFieldText { // name field from register screen
assertIsDisplayed()
performTextInput(userName)
}
emailRFieldText { // email field from register screen
assertIsDisplayed()
performTextInput(goodEmail)
}
passwordRFieldText { // password field from register screen
assertIsDisplayed()
performTextInput(goodPassword)
}
performCloseSoftKeyBoard() // close soft keyboard to avoid issues
registerButton { // register button from register screen
assertIsDisplayed()
// verify if the button has the correct text
assertTextEquals(getResourceString(R.string.accept))
performClick()
}
}
}
...

Section 9: Running our test-cases

Sequentially, now that we have test-cases ready, we can run them. To do this, we need to run the following command in the terminal, always keep in mind the flavors in your projects:

./gradlew connectedAndroidTest

Or you can use the Android Studio interface, as shown in the image below:

Just right-click in the test file and then select the Run DojoInstrumentedTest option.

After a few minutes the Test suite will be concluded and Android Studio shows the report as output, including the duration and the device where was tested for each one.

Section 10: Common issues

When you start to write your test-cases, you will probably face some issues. Always its good practice to read the documentation and search for solutions on the internet. In this section, I will present some common issues you may face when writing your test-cases.

Typical mistakes

  • Check your dependencies: In almost all cases, the issues are related to dependencies. Make sure that you are using versions of the dependencies that are compatible with each other.
  • Make sure that you are importing the correct classes in your workflow. I put a lot of work into trying to figure out why my test-cases were not working, and the root of the problem was that I was importing the wrong ‘run’ block, the wrong ‘step’ block, or the wrong ‘RunWith’ annotation.
  • It’s recommended to use a semantic testing tag instead of the test tag due to the fact that the test tag is deprecated.
  • Check the testTag name, be careful with duplicated tags, it must be unique.
  • A Kaespresso test-case continues on the latest screen of the previous test-case, that can be totally undesirable. To avoid this, you can use the ‘before’ block to reset the app state or use back actions to navigate back to the previous screen until you have exited the application.

Conclusion

To conclude, the instrumental test is an awesome tool to ensure your app meets the acceptance criterias from your user stories. Kaspresso is a very powerful and amazing framework. It is very easy to use and it has many features that can help us to write test-cases and complete the whole workflow. I hope this article/blog/tutorial will help you to start writing your test-cases using Android-Kaspresso library to become a Testing Automation Engineer.

You can find the Android-Compose Application Repository here.

Thank you for being here, please comment down your views, if any mistakes found the article will be updated

Reference

--

--

Freddy Domínguez

Peruvian #Software #Engineer CIP 206863, #Business #Intelligence #Data #Science. I work with people to create ideas, deliver outcomes, and drive change