Blog Infos
Author
Published
Topics
,
Published

Notifications are a very important part of Android apps, showing relevant information, informing when the user needs to take action and even not letting them losing appointments. Once this aspect is critical for some apps, it is indispensable to think about adding tests to validate its behavior.

One way of testing notifications is to create an interface not dependent on Android-related components to Unit test all the notification structure and behavior without really sending it. Although this is a nice way to test it, it does not cover the nuances of Android environment, such as “was it send at the appropriate time?”, “are the actions opening the correct screen?” and so on.

Another way of testing them is using UiAutomator, which allows us to create cross-app interactions, meaning we can manually open the notification drawer and do a lot of actions to validate the behavior. Back in 2019, I implemented some tests using this tool, but now in 2022 I decided to look for an alternative for two main reasons:

  • The tests were flaky — sometimes the notification was not visible in the drawer, the string for “Clear” notifications varies in different versions of Android and so on
  • UiAutomator is outdated — the last release was in 2018

So how can we test notifications reliably?

Using the NotificationManager for testing

Yes, the answer in fact is easier than we thought. We can use the NotificationManager with Espresso or Compose Tests to validate all the expected behaviors. Let’s take a look on how to create a simple test using it:

@Test
fun test_myNotification() {
// Send the notification
val id = 13
val name = "Testing my notification"
myNotification.send(id = id, name = name)
// Validate the notification info
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
with(manager.activeNotifications.first()) {
assertEquals(id, this.id)
assertEquals(name, this.notification.extras[Notification.EXTRA_TEXT])
}
}

In the code above, we send a notification and use the NotificationManager to get the first notification and assert that both the name and the id matches the launched one. The NotificationManager only access the notifications launched by our app, so there is no problem if the drawer has dozen of them or if your notification is not visible at the top.

Clearing the notifications

The notifications launched will stay in the drawer as a regular one if no action was taken. Therefore it is good practice to clean all the notifications after the execution to avoid conflicting with other tests:

@After
fun tearDown() {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.cancelAll()
}

Simple as that.

Idling Resources

Since the test code runs very fast, it is likely that between the send() and the assert the notification is not yet already in the NotificationManager list and the test will fail sometimes. In order to avoid this, we need to find a way to synchronize the test and the app. The Android Developers website has a documentation about how to use Espresso Idling Resources and recently Jose Alcérreca  published a great article on alternatives while using Jetpack Compose.

 

In our example, let’s assume we are developing our screens using Compose and see how waitUntil can help us here.

@Test
fun test_myNotification() {
// Send the notification
val id = 13
val name = "Testing my notification"
myNotification.send(id = id, name = name)
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Wait until the active notification list has a new one
composeTestRule.waitUntil { manager.activeNotifications.isNotEmpty() }
// Validate the notification info
with(manager.activeNotifications.first()) {
assertEquals(id, this.id)
assertEquals(name, this.notification.extras[Notification.EXTRA_TEXT])
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Put Your Tests on a Diet:Testing the Behavior and Not the Implementation

How do you write tests? How much time do you spend writing tests? And how much time do you spend fixing them when refactoring?
Watch Video

Put Your Tests on a Diet:Testing the Behavior and Not the Implementation

Stelios Frantzeskakis
Staff Engineer
PSS

Put Your Tests on a Diet:Testing the Behavior and Not the Implementation

Stelios Frantzeska ...
Staff Engineer
PSS

Put Your Tests on a Diet:Testing the Behavior and Not the Implementation

Stelios Frantzes ...
Staff Engineer
PSS

Jobs

The updated code now waits until the active notification list is not empty before doing the assertion. The default timeout is 1 second, but the value is customizable. To learn more about the waitUntil API, please take a look at the official docs.

Testing notification actions
Content Intent

Now that we are able to test the notifications title and content, the next step would be testing if the actions related to them are working. Let’s imagine that we have a notification that when clicked shows a screen with more information about it. The test code would look like:

@Test
fun test_whenNotificationIsClickedOpensDetails() {
// Send the notification
val id = 13
val name = "New reward!"
myNotification.send(id = id, name = name)
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Wait until the active notification list has a new one
composeTestRule.waitUntil { manager.activeNotifications.isNotEmpty() }
// Run the PendingIntent related to notification content
manager.activeNotifications.first().notification.contentIntent.send()
// Validate the screen is shown
composeTestRule.onNodeWithText("My screen title").assertIsDisplayed()
composeTestRule.onNodeWithText(name).assertIsDisplayed()
}

In the code above, after sending the notification and getting it from the notification list we call contentIntent.send(). This statement will take the PendingIntent and execute it right away. Then we assert that the screen with the relevant data was opened.

Actions Buttons Intent

In the next example, let’s imagine that we have a notification informing that a new message was received by our app. And, for quick interaction, we have an action button “Mark as read” that updates the message state and dismissed the notification. The code for testing this behavior may look like:

@Test
fun test_markAsReadViaNotificationActions() {
// Send the notification
val id = 99
val name = "New message received from Bruno"
myNotification.send(id = id, name = name)
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Wait until the active notification list has a new one
composeTestRule.waitUntil { manager.activeNotifications.isNotEmpty() }
// Run the PendingIntent in the "Mark as read" action button
manager.activeNotifications.first().notification.actions[0].actionIntent.send()
// Validate the message is mark as read
Assert.assertTrue(messageRepository.getLatestMessage(id = id).isRead())
// Validate the notification was dismissed
Assert.assertTrue(manager.activeNotifications.isEmpty())
}

This time, after getting the notification we access the first Notification.Actionfrom the list actions list and execute the PendingIntent. After that we validate that our message repository is now considering the message as read.

Conclusion

Using NotificationManager allows us to have more control over the notifications launched by our app, making it easier to test its behavior in different scenarios. In my personal experience, I was able to create easier and more reliable tests using this tool compared to UiAutomator.

What’s next?

As usual, I would love to share a Pull Request from my personal Android app, Alkaa — Tasks Simplified. This change adds a lot of Instrumented Tests, including notifications.

 

Thanks a lot for reading my article! ❤️

This article was originally published on proandroiddev.com on May 03, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Every good Android application should be well tested to minimize the risk of error…
READ MORE
blog
In this article we’ll go through how to own a legacy code that is…
READ MORE
blog

Running Instrumented Tests in a Gradle task

During the latest Google I/O, a lot of great new technologies were shown. The…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu