In case you haven’t used App Shortcuts in Android yet, it’s an awesome feature that allows us to provide our users with a way to quickly access parts of our apps from the home screen of their device. As developers, we can make these shortcuts either static (meaning they’re statically defined in an XML file) or dynamic (meaning they’re dynamically added and removed at runtime). Currently in the Buffer app for Android we have 2 types of dynamic shortcuts:

A shortcut that allows you to access the composer

Three shortcuts for the 3 previously selected profiles (this one is in currently only available in beta!)

But why do we need to write tests for this?

All of these shortcuts are dynamic, meaning that they are only accessible when you are signed in to an account. This means that we have to add them when a user signs-in and removes them when the user signs out – thankfully for us, the shortcut api makes it super easy to do this. However, just like any features of an application, this could easily get broken in the future – this means that if these do somehow break:

Users may not be able to see the shortcuts when they are signed in

Users may still be able to see the shortcuts when they have signed out. Without being signed in, it could be quite confusing for these shortcuts to show – so we want to be sure they’re never shown in this situation

Users may be shown the wrong recently selected profiles – this could be quite annoying and defeats the point of the shortcuts in the first place!

This sounds like a perfect situation to write tests to ensure that these situations never occur. However, because these shortcuts are outside of our application it means we can’t use Espresso to test them.

But! Whilst we can’t use Espresso, luckily we have what is known as UI Automator available to us, it’s all good!

If you haven’t used UI Automator before, it’s essentially a testing tool that allows us to interact with system-wide components – it’s useful for testing things outside of your app (such as notifications) or components invoked by the system (such as permission dialogs). Because of this, it means we can use it to test our App Shortcuts ??

Nice! But how do I do this?

If you don’t want to learn about how to do this for yourself, we’ve bundled some handy functions into our testing utility library Biscotti that will allow you to test for app shortcuts. In this library you’ll find two handy methods:

assertAppShortcutsExist()

This method allows you to check that an app shortcut exists for your application. For example:

BiscottiShortcuts.assertAppShortcutsExists("Buffer", InstrumentationRegistry .getTargetContext().getString(R.string.shortcut_compose_update))

assertAppShortcutsDoNotExist()

This method allows you to check that an app shortcut does not exist for your application. For example:

BiscottiShortcuts.assertAppShortcutsDoNotExist("Buffer", InstrumentationRegistry .getTargetContext().getString(R.string.shortcut_compose_update))

Both of these methods also allow you to pass in multiple strings for checking multiple labels, just in case you need a test to check multiple related app shortcuts.

Cool! I want to know how this works!

So let’s take a dive into how we’re doing this with UI Automator, here is the complete code for checking that an app shortcut exists:

fun assertAppShortcutsExists(appName: String, vararg shortcutLabels: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) findAppIcon(device, appName).longClick() shortcutLabels.forEach { if (!device.hasObject(By.text(it))) { Assert.fail("The specified shortcut was not found") } } } } private fun findAppIcon(device: UiDevice, appName: String): UiObject { device.pressHome() if (device.hasObject(By.desc("Apps"))) { device.findObject(By.desc("Apps")).click() } val appDrawer = UiScrollable(UiSelector().scrollable(true)) appDrawer.scrollForward() appDrawer.scrollTextIntoView(appName) return device.findObject(UiSelector().text(appName)) }

We have two methods here; assertAppShortcutsExists() which is used by our application to test the app shortcut and findAppIcon() which is used for finding the app icon on the device. Let’s begin by taking a look at the assertAppShortcutsExists() method.

Wait, won’t this fail on devices running < 7.1?

Good question! You’ll notice here that the logic within this method is only used if the Build version of the device is Android 7.1 or newer – this is because app shortcuts are only available on there, so we don’t want these tests running on older versions of Android as they will fail. It would be nicer to have the test not run at all on devices that are running less than 7.1, but I’m not sure of a way to do this yet ?

Navigating to the application icon

The first step we need to carry out is navigating to the icon for our application, this is so that we can invoke the action to show the app shortcuts. We begin by retrieving a reference to the current device as a UiDevice instance:

val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

We then use this value when calling the findAppIcon() method. This is used, along with the given app name, to find the application icon that we want to check for app shortcuts. Within this method we begin by using the pressHome() method to press the home button on the device, this will cause our app to be exited and the home screen of the device to be shown.

device.pressHome()

Once we’re on the home screen of our device, we need to find the icon of our app. Now, the way we do this can differ between devices – some devices will have an app launcher icon, yet some will have the pixel style launcher that swipes up from the bottom of the screen. To account for this we’re going to first check if the device has the App Launcher button like so:

device.hasObject(By.desc("Apps"))

If the device has this button, you’ll notice that we go ahead and click it:

device.findObject(By.desc("Apps")).click()

If the device doesn’t have this button then it’s ok because we make use of the UiScrollable class to scroll up on the screen, this will bring up the pixel style launcher, revealing the apps on the devices. We still need to use this UiScrollable class here even if the device does have the app launcher button as we will need to scroll to the app icon anyway – this just means we are now accounted for both situations ?

Note: I haven’t yet tested this on devices that swipe horizontally. These tests will only be run on Android 7.1 above which I think makes this behaviour less common, but you may need to tweak this code if your tests are running on devices that use this approach.

val appDrawer = UiScrollable(UiSelector().scrollable(true)) appDrawer.scrollForward()

Next, we need to actually scroll to our app icon, this is done by the appDrawer.scrollTextIntoView(appName) call. Once our app icon is found it is returned by again using the findObject() method.

appDrawer.scrollTextIntoView(appName) return device.findObject(UiSelector().text(appName))

Asserting the object state

If we head back on over to our assertAppShortcutsExists() method, we now have the reference to our app icon so we immediately perform a longClick() action on it. A long click is what causes our app shortcuts to be shown. Now that these are showing, we can go ahead and check that our desired shortcuts are shown. We again use the hasObject() method to check whether or not the given shortcut label is shown and if not, we cause the test to fail.

if (!device.hasObject(By.text(it))) { Assert.fail("The specified shortcut was not found") }

The assertAppShortcutsDoNotExist() method works in the same way except we just assert that the given shortcut label is not shown on the screen.

Conclusion

It was exciting for me to discover that it was possible to write automated tests for app shortcuts and it feels great knowing that these are now more protected against future regressions. I haven’t been able to try this yet on a wide range of devices (it passes in our test suit on the devices we are using) so I’d be interested to know if there are any cases for you where this may not work out as intended!