Table of content

  1. Introduction
  2. Specific of Android testing
  3. Problems of testing notifications
  4. Frameworks and tools for UI testing in Android
  5. Practise: Testing notifications
    5.1. Verification essential parts of notification
    5.2. Verification tap on a notification
    5.3. Verification tap on an action
    5.4. Verification of the notification bundle
    5.5. Verification the notification badge feature
  6. Resources

Introduction

Testing allows us to improve the quality of the product because after any changes you can be sure that all your basic functionality works correctly.

We can test different parts of our application, and one of these parts are notifications (almost all application use them for different purposes). Recently, I wrote, “Guide: Android notifications” and if you are not familiar with this topic, I recommend to read it, because it allows you to improve knowledge about this topic.

The NotificationDemo repository contains demo application for Android notifications and UI tests from the project. Additionally, the PR: “Adding UI tests” shows all changes for adding the UI tests and Kotlin to the project.

Specific of Android testing

Before testing Android application, we should discuss few specific aspects for this platform.

  • We have two different types of tests: local and instrumentation.
  • The local tests are run on local JVM, it means that we can run these tests on our development machine or CI (Continuous Integration) without additional devices.
  • The instrumentation test cases are run on a device or an emulator. If we want to use CI (Continuous Integration), we should connect additional devices for these tests or run the emulator.
  • Local tests are much faster than instrumentation ones. As a result, we should have much more local tests which can verify business logic of the product, for efficient testing.
  • Instrumentation tests can be splitted to UI and non-UI instrumentation test cases.
  • Non-UI instrumentation tests are much faster than UI ones because we don’t need to wait for loading Activities, Fragments and View. In addition to it, UI tests usually wait for loading data from any data sources.

Problems of testing notifications

Coming back to the notifications, we should verify many different scenarios:

  • displaying a notification
  • interacting with notification by tapping to notification and actions
  • interacting with bundle notification
  • supporting notification badges

Some of these scenarios cannot be important for some applications. Another one can provide system features, like “Notification Badge”, but if it’s important to you, it can be tested. I mean that when some of the developers change channel setting and turn off the supporting of notification badge, the test can fail and it will be an indication for you.

I would like to show you testing all scenarios. However, some of them can be optional for your product.

Frameworks and tools for UI testing in Android

Let us explore tools which we can use for creating UI test cases for notification verification. We can use different frameworks for testing Android applications:

  • Espresso is a framework for emulating user behaviour within an application.
  • UiAutomator framework allows us to interact with a system, different applications, notifications, hardware buttons, etc.
  • Appium is an open source test automation framework for using with native, hybrid and mobile web apps.
  • etc.

We will discuss how to test notifications with Espresso and UiAutomator frameworks and I will also tell you about basic possibilities of these frameworks.

Espresso

Espresso is a framework for emulating user behaviour within an application. We can interact with different widgets: Buttons, CheckBox, EditText, RecyclerView, etc. One of the most significant benefits of this framework is writing tests without sleep, because Espresso has Idling Resource mechanism, which allows us to do actions and verifications of different components from different activities without any problem and workarounds, like sleep.

If you not familiar with Espresso I can recommend you few resources:

UiAutomator

UiAutomator framework allows us to interact with a system, different applications, notifications, hardware buttons, etc.

If you are not familiar with UiAutomator, I can recommend you few resources:

UiAutomatorViewer

UiAutomatorViewer demo

UiAutomatorViewer demo

UiAutomatorViewer is a tool which provides information about any view on the screen; it means that we can explore IDs of different applications, like Launcher. It allows you to create test cases with UiAutomator which interacts with any application. In this way, we can test the “notification badge” feature, open links from any application via your browser (if you created browser).

You can run this UiAutomatorViewer from the {android-sdk}\tools\bin folder.

We can combine Espresso and UiAutomator frameworks even in a single test and create efficient and powerful test cases.

Practise: Testing notifications

Let us explore how we can add tests for notifications verification. We have many cases for checking of any notifications:

  • displaying essential information
  • tapping on the notification
  • tapping on the action
  • interacting with notification bundle
  • interacting with notification badge

Surely, you can have other cases, but I am trying to show you the general use cases.

First of all, I recommend to create additional methods which can help us with testing notifications.

The clearAllNotifications method is needed for clearing all notifications in notification ares. We archiving it with “CLEAR ALL” button.

UiAutomatorViewer "Clear All" buttom

Clear all button in notification area

private fun clearAllNotifications() {
    val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    uiDevice.openNotification()
    uiDevice.wait(Until.hasObject(By.textStartsWith(expectedAppName)), timeout)
    val clearAll: UiObject2 = uiDevice.findObject(By.res(clearAllNotificationRes))
    clearAll.click()
}
In this case, we use UiAutomator framework which allows us to interact with Android OS. This method opens notification area, waits for notification (because this framework hasn’t got smart waiting mechanism, like IdlingResources in Espresso). Afterwards, we are looking for the “CLEAR ALL” button and click on it.
The clickOnSendNotification ViewAction is needed for tapping to the view with send_button ID for one of items in the RecyclerView.
private fun clickOnSendNotification() : ViewAction {
    return object : ViewAction {
        override fun getDescription(): String {
            return "Click on the send notification button"
        }

        override fun getConstraints(): Matcher<View> {
            return Matchers.allOf(isDisplayed(), isAssignableFrom(Button::class.java))
        }

        override fun perform(uiController: UiController?, view: View?) {
            view?.findViewById<View>(R.id.send_button)?.performClick()
        }
    }
}
Here we verify that a button with the send_button is displayed and we click on it.
The last interesting moment of testing notification is  floating-notification. You can see this situation when notification is displayed and overlaps part of an application.
Floating notification

Floating notification

We can disable it using the adb command for a device or an emulator

adb shell settings put global heads_up_notifications_enabled 0

However, if we want to have floating notification feature enabled ,we can add swipe to notification for hiding current notification. Sorry for this code – I am ashamed of this workaround, but sometimes it can be helpful. However, I strongly recommend to turn of the  floating-notification feature for testing purposes.
/**
 * Workaround for swiping down the floating notification.
 * As an alternative, we can turn off them using the
 * <pre>
 * {@code adb shell settings put global heads_up_notifications_enabled 0}
 * </pre>
 */
@SuppressWarnings("unused")
private fun hideNotification() {
    uiDevice.swipe(
            200,
            200,
            200,
            100,
            5
    )
}

Verification essential parts of notification

We can verify the content of our notification with Espresso and UiAutomator frameworks because notification is a part of a system and we cannot verify it with Espresso. Here we click on “SEND NOTIFICATION” button with Espresso framework and verify the content of notification with UiAutomator.
Verification essential parts of notification

Verification essential parts of notification

private val uiDevice by lazy { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) }


@Rule @JvmField
val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java)


@Test
fun shouldSendNotificationWhichContainsTitleTextAndAllCities() {
    val expectedAppName = activityRule.activity.getString(R.string.app_name)
    val expectedAllCities = activityRule.activity.getString(R.string.notification_action_all_cities)
    val expectedTitle = activityRule.activity.getString(R.string.notification_title)
    val expectedText = DummyData.getCityById(amsterdamId).description

    onView(withId(R.id.list))
            .perform(actionOnItemAtPosition<CityViewHolder>(firstItemPos, clickOnSendNotification()))

    uiDevice.openNotification()
    uiDevice.wait(Until.hasObject(By.textStartsWith(expectedAppName)), timeout)
    val title: UiObject2 = uiDevice.findObject(By.text(expectedTitle))
    val text: UiObject2 = uiDevice.findObject(By.textStartsWith(expectedText))
    val allCities: UiObject2 = uiDevice.findObject(By.res(expectedAllCitiesActionRes))
    assertEquals(expectedTitle, title.text)
    assertTrue(text.text.startsWith(expectedText))
    assertEquals(expectedAllCities.toLowerCase(), allCities.text.toLowerCase())

    clearAllNotifications()
}

Verification tap on notification

When we click on the notification, we should open the information about city. It means that we can use Espresso and UiAutomator for verification of this case. First of all, we should send notification and open notification area. Later on, we should find our notification by text and click on it. Finally, we can verify information about the city inside the app.
Verification tap on notification

Verification tap on notification

private val uiDevice by lazy { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) }


@Rule @JvmField 
val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java)


@Test
fun shouldClickOnNotificationOpenDetailsScreen() {
    val expectedAppName = activityRule.activity.getString(R.string.app_name) 
    val expectedText = DummyData.getCityById(amsterdamId).description
    val expectedSource = activityRule.activity.getString(R.string.source)
    onView(withId(R.id.list))
            .perform(actionOnItemAtPosition<CityViewHolder>(firstItemPos, clickOnSendNotification()))

    uiDevice.openNotification()
    uiDevice.wait(Until.hasObject(By.textStartsWith(expectedAppName)), timeout)
    val text: UiObject2 = uiDevice.findObject(By.textStartsWith(expectedText))
    text.click()
    uiDevice.wait(Until.hasObject(By.text(expectedText)), timeout)

    onView(withId(R.id.description_textView))
            .check(matches(withText(expectedText)))

    onView(withId(R.id.source_textView))
            .check(matches(withText(expectedSource)))
}

Verification tap on an action

Actions are the powerful mechanism of notifications. In our case, we have an “ALL CITY” action which opens the list with all cities. The steps for verification are pretty similar to the previous case, and finally, we can check part of the main screen. A unique part of the list of cities is a button “Send all notification as a bundle”. However, we can check that our list contains all needed cities.
Verification tap on an action

Verification tap on an action

private val uiDevice by lazy { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) }


@Rule @JvmField 
val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java)


@Test
fun shouldClickOnAllCitiesOpenAllCitiesList() { 
    val expectedAppName = activityRule.activity.getString(R.string.app_name)
    val allNotificationAsBundleText: String =
            activityRule.activity.getText(R.string.title_send_all_notifications).toString()

    onView(withId(R.id.list))
            .perform(actionOnItemAtPosition<CityViewHolder>(firstItemPos, clickOnSendNotification()))

    uiDevice.pressBack()

    uiDevice.openNotification()
    uiDevice.wait(Until.hasObject(By.textStartsWith(expectedAppName)), timeout)
    val allCities: UiObject2 = uiDevice.findObject(By.res(expectedAllCitiesActionRes))
    allCities.click()
    uiDevice.wait(Until.hasObject(By.text(allNotificationAsBundleText)), timeout)

    onView(withId(R.id.send_all_notifications))
            .check(matches(withText(R.string.title_send_all_notifications)))
}

Verification of the notification bundle

Notification can be grouped for better UX and readability on different devices. We should swipe for bundle notification for expanding list of notifications in this bundle. It means that after notification verification we can swipe down and verify one of notification with click on this notification for checking information about the city.

I commented hideNotification() because it depends on your way of working with floating notification.

Verification of the notification bundle

Verification of the notification bundle

private val uiDevice by lazy { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) }


@Rule @JvmField 
val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java)


@Test
fun shouldClickOnFirstNotificationOfNotificationBundle() {
    val expectedAppName = activityRule.activity.getString(R.string.app_name) 
    val expectedText = DummyData.getCityById(amsterdamId).description
    
    onView(withId(R.id.send_all_notifications))
            .perform(click())

    /*hideNotification()*/
    uiDevice.openNotification()
    uiDevice.wait(Until.hasObject(By.textStartsWith(expectedAppName)), timeout)
    val notificationHeader: UiObject2 = uiDevice.findObject(By.res(notificationHeaderRes))
    notificationHeader.swipe(Direction.DOWN, 0.05f)

    val amsterdamNotification: UiObject2 = uiDevice.findObject(By.text(expectedText))
    amsterdamNotification.click()
    uiDevice.wait(Until.hasObject(By.text(expectedText)), timeout)

    onView(withId(R.id.description_textView))
            .check(matches(withText(expectedText)))

    clearAllNotifications()
}

Verification the notification badge feature

“Notification badge” feature allows us to long press the icon and see a notification near the icon. We should open all applications and find our application icon. Afterwards, we should long press on the icon, and finally, we can click on notification and verify the behaviour of interacting with notification.

I commented hideNotification() because it depends on your way of working with floating notification.

Verification the notification badge feature

Verification the notification badge feature

private val uiDevice by lazy { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) }


@Rule @JvmField 
val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java)


@Test
fun shouldClickOnAppIconAndVerifyNotificationBadge() {
    val expectedAppName = activityRule.activity.getString(R.string.app_name) 
    val expectedText = DummyData.getCityById(amsterdamId).description
    val allAppsLauncherRes = "com.google.android.apps.nexuslauncher:id/drag_indicator"    

    onView(withId(R.id.list))
            .perform(actionOnItemAtPosition<CityViewHolder>(firstItemPos, clickOnSendNotification()))

    uiDevice.pressHome()
    /*hideNotification()*/
    val allApps : UiObject2 = uiDevice.findObject(By.res(allAppsLauncherRes))
    allApps.click()

    uiDevice.wait(Until.hasObject(By.text(expectedAppName)), timeout)
    val app: UiObject2 = uiDevice.findObject(By.text(expectedAppName))
    app.longClick()

    val amsterdamNotification: UiObject2 = uiDevice.findObject(By.text(expectedText))
    amsterdamNotification.click()
    uiDevice.wait(Until.hasObject(By.text(expectedText)), timeout)
    onView(withId(R.id.description_textView))
            .check(matches(withText(expectedText)))
}
I described the most popular use cases which we should check if we want to test sending and interacting with notifications.

Resources