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 Adding UI tests Pull Request 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 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.
Practice: 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.
private fun clearAllNotifications() {
val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
uiDevice.openNotification()
uiDevice.wait(Until.hasObject(By.textStartsWith(expectedAppName)), timeout)
uiDevice.findObject(By.res(clearAllNotificationRes))
.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.
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.
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.
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.
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.
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
The "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.
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.