Unit Testing Android BLE Code with RxAndroidBle
In this post, we look at what RxAndroidBle offers as far as unit testing is concerned, and we’ll also walk through a case study of unit testing a common BLE business logic.
Seasoned Android developers will no doubt have heard of RxJava—a JVM implementation of Reactive Extensions (ReactiveX, or Rx for short). For developers whose projects follow this Rx paradigm and also require interacting with BLE devices, RxAndroidBle is a great library that solves a lot of pain points. It abstracts away the complexities of having to deal with the problematic Android BLE APIs, while still allowing developers to write fluent and idiomatic Rx code. Given all the strengths of RxAndroidBle, we’ve found that it’s not always straightforward to beginners what tools are available to developers, and what options are out there when it comes to unit testing code written using RxAndroidBle.
Challenges of Unit Testing BLE Code
At Punch Through, we’re huge fans of unit testing. We believe that writing unit tests is crucial for safeguarding code behavior from unintended changes, and think that it’s a great way to verify expectations and assumptions.
Unit testing a codebase that touches the Android BLE APIs is far from an easy task—there’s a ton of work that needs to go into mocking remote BLE peripherals, their corresponding connection handles, and various callbacks surfaced to the client layer. Fortunately, all of RxAndroidBle’s publicly accessible main classes are interfaces—this allows us to easily mock them up ourselves for the purposes of unit testing.
To help developers get started quickly, RxAndroidBle has done most of the heavy lifting in the form of a separate MockRxAndroidBle artifact that contains some readily available mocks for their commonly used interfaces. However, there are no clear instructions on how to use these mock classes at the time of writing, so read on to find out more about how to use these classes to write effective unit tests!
In our experience, a common flow of writing unit tests for BLE code looks something like this:
- Set up the test environment or context. Is the device in a connected state? Is BLE available?
- Define test expectations. What is the expected outcome for the given operation we’re testing?
- Perform the action to be tested. Have the test code call into the functionality that is being tested, for instance the test could attempt to write data to a characteristic.
- Simulate an external BLE event to trigger a desired response in tested code (situational). This can be an event to trigger a desired outcome, like a scan result being available, an incoming notification or indication, a characteristic read success, etc.
- Assert that the outcome is what we expect it to be. If a write operation was performed and a device disconnection happened, we would expect an error to be emitted by the subscribed
Observable
. Otherwise, we expect theObservable
to complete normally.
Setup and Tools for Unit Testing
In order to get started unit testing your code written with RxAndroidBle, you’ll need the following dependencies and configurations in your app module’s build.gradle. This is typically your app/build.gradle
file. Note that you can replace the version numbers in the snippet shown below with whatever the latest versions of those artifacts are.
android { // ... kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } testOptions { unitTests.includeAndroidResources = true } // ... } dependencies { // ... implementation 'com.polidea.rxandroidble2:rxandroidble:1.10.5' testImplementation 'junit:junit:4.12' testImplementation 'androidx.test:core:1.2.0' testImplementation 'org.robolectric:robolectric:4.3.1' testImplementation 'com.polidea.rxandroidble2:mockclient:1.10.5' testImplementation 'org.mockito:mockito-core:2.19.0' // ... }
You probably already have the RxAndroidBle dependency declared in your build.gradle file, so here’s a quick rundown of what the rest of the testing-specific dependencies are:
- JUnit 4: Unit testing framework that provides a means to run tests and perform assertions on test expectations. This should already be included by default in your app/build.gradle file.
- AndroidX Test and Robolectric: Required by MockRxAndroidBle in order to execute real Android framework code on your local JVM instead of requiring an emulator or a physical device.
- MockRxAndroidBle: Contains conveniently mocked classes for common RxAndroidBle interfaces. We’ll go through examples in the next section on why this artifact is useful for unit testing with RxAndroidBle.
- Mockito: An optional mocking framework that allows us to control certain behaviors of the Android SDK that aren’t usually accessible. For example, we can use Mockito to create a mock
BluetoothAdapter
that always returnsfalse
when itsisEnabled()
method is invoked.
Mocking RxAndroidBle Objects
Before we dive into a concrete example of unit testing code written using RxAndroidBle, there are some important tidbits to know about the MockRxAndroidBle artifact. It provides convenient ready-for-use mocks for the 3 main interfaces most developers will use from the main library:
- RxBleClient: The “
BluetoothManager
” type class for RxAndroidBle. This interface provides an entry point for querying the Bluetooth adapter’s state, as well as kicking off a BLE scan. - RxBleDevice: The handle for a remote BLE device, similar to the Android SDK’s
BluetoothDevice
object. - RxBleConnection: The handle for an active BLE connection. This interface behaves much like the Android SDK’s
BluetoothGatt
object, and supports service discovery, setting up indications/notifications, reading/writing characteristics, etc.
The corresponding mocks provided by MockRxAndroidBle are:
- RxBleClientMock: An instance of this class is obtained via the
RxBleClientMock.Builder
class. A mockedRxBleClient
allows us to directly control behaviors like scanning and connecting while writing unit tests. - RxBleDeviceMock: An instance of this class is obtained via the
RxBleClientMock.DeviceBuilder
class, which lets us specify the mock device’s name, MAC address, RSSI, and scan record. More importantly, it allows us to specify a notification source which is essentially anObservable
(typically someSubject
implementation) that we have control over, and we can simulate an incoming notification or indication by triggering anonNext()
event on saidObservable
. - RxBleConnectionMock: An instance of this class can be initialized directly, but it’s usually unnecessary to do so ourselves — the
RxBleDeviceMock
instance that was constructed usingRxBleClientMock.DeviceBuilder
would already have aRxBleConnectionMock
object instantiated as one of its internal member fields.
Given all the above, we can very easily construct a mock RxBleDevice
that suits our testing requirements. The snippet below shows the setup required for a mock device that meets the following requirements:
- MAC address of 00:11:22:33:44:55
- Device name of “HR Monitor”
- Scan record of
[0x00]
- RSSI of -50 dBm
- A service which contains a writable characteristic and an indication characteristic, the latter of which will emit 2 indication events when it’s subscribed to: [0x00] followed by [0x01]
val mockDevice = RxBleClientMock.DeviceBuilder() .deviceMacAddress("00:11:22:33:44:55") .deviceName("HR Monitor") .scanRecord(byteArrayOf(0x00)) .rssi(-50) .addService( UUID.fromString(MOCK_SERVICE_UUID), RxBleClientMock.CharacteristicsBuilder() .addCharacteristic( UUID.fromString(MOCK_WRITE_CHARACTERISTIC_UUID), byteArrayOf(), RxBleClientMock.DescriptorsBuilder().build() ) .addCharacteristic( UUID.fromString(MOCK_INDICATE_CHARACTERISTIC_UUID), byteArrayOf(), RxBleClientMock.DescriptorsBuilder() .addDescriptor( UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID), byteArrayOf() ) .build() ) .build() ) .notificationSource( UUID.fromString(MOCK_INDICATE_CHARACTERISTIC_UUID), Observable.just( byteArrayOf(0), byteArrayOf(1) ) ) .build()
Now that we’re able to mock the commonly used objects in RxAndroidBle, let’s see how we can put them to use in an actual unit test function.
Injecting Mocks for Unit Testing
As is common practice with unit testing, we want to isolate the test scope to be laser-focused on a unit of functionality in an ideal scenario. This means that all our code’s interactions with external libraries or code that we don’t own should ideally be mocked, because the onus is on those external parties to unit test their implementations. In the case of unit testing interactions with RxAndroidBle, we strongly recommend that you wrap all your usages of RxAndroidBle in a BluetoothManager
type class instead of calling into its APIs directly from your user-facing classes.
The main advantage of doing so becomes apparent when we need a way to inject a different RxAndroidBle object depending on whether we’re in a unit test environment or not. Since the RxBleClient
interface is the main entry point into most RxAndroidBle functionalities, we’ll focus on it for this section of the post.
If we have a class wrapping all our calls to RxAndroidBle, we can easily set the default RxBleClient
(obtained via RxBleClient.create(context.applicationContext)
) as the default argument for the client to use. The code snippet below shows a simple example of wrapping the call to RxBleClient
’s state in a BluetoothManager
class. Notice how the client property is set to the concrete library implementation by default.
class BluetoothManager( context: Context, private val client: RxBleClient = RxBleClient.create(context.applicationContext) ) { val clientState: RxBleClient.State get() = client.state // ... }
In a unit test environment, we’d instead inject a mocked RxBleClient
, overriding the default argument value. Here’s how a very simple example looks like:
class BluetoothManagerTests { private val context = ApplicationProvider.getApplicationContext<Context>() @Test fun state_returnsExpectedValue() { val mockClient = RxBleClientMock.Builder().build() val bluetoothManager = BluetoothManager(context, mockClient) assertTrue(RxBleClient.State.Ready, bluetoothManager.state) } }
Once your codebase’s interaction with RxAndroidBle is set up in such a way that allows you to inject a mocked RxBleClient
for unit testing purposes, you’re ready to move on to write more advanced tests.
Case Study: Unit Testing Parsing Logic of Advertisement Packet
Most BLE apps already scan for nearby peripherals and surface them as scan results. These scan results often have advertisement records that can contain meaningful data nested within them. A common practice that many BLE products follow is storing custom data as manufacturer specific data. In this section, we’ll walk through a case study of unit testing an app’s parsing logic for BLE advertisement packets.
We’ll assume that this app is scanning for devices that’ll contain some manufacturer specific data in their advertisement packets, and that the manufacturer specific data should be parsed as a UTF-8 string representing the peripheral’s serial number. We’ll also assume that the manufacturer identifier the app is looking for is hardcoded to 0x00E0
(224 in decimal, which is Google’s identifier).
Given our assumptions above, this app will likely have something like this implemented:
data class Peripheral( val serialNumber: String, val device: RxBleDevice ) class BluetoothManager( private val context: Context, private val client: RxBleClient = RxBleClient.create(context.applicationContext) ) { fun scan(): Observable<Peripheral> { val scanSettings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() return client.scanBleDevices(scanSettings) .filter { // We only care about devices with 224 as their manufacturer identifier it.scanRecord.getManufacturerSpecificData(224) != null } .map { // Parse serial number in UTF-8 and create a Peripheral object val serialNumber = it.scanRecord.manufacturerSpecificData[224] .toString(Charsets.UTF_8) Peripheral(serialNumber, it.bleDevice) } } }
The scan()
function above returns an Observable
that — when subscribed to — scans for all advertising BLE devices in the vicinity, but only devices that have 224 as their manufacturer identifier will be emitted as scan results. Each scan result is wrapped in a Peripheral
object that conveniently contains a serialNumber
property, representing the contents of the manufacturer specific data parsed as a UTF-8 string. Notice how the BluetoothManager
class can be injected with a mocked RxBleClient
if needed.
We then proceed to create a test function called BluetoothManagerTests
, and a first pass may look something like the following:
@Test fun scan_returnsPeripherals_withSerialNumbersParsed() { val context = ApplicationProvider.getApplicationContext<Context>() val mockClient = RxBleClientMock.Builder().build() val bluetoothManager = BluetoothManager(context, mockClient) bluetoothManager .scan() .test() .assertNoErrors() .assertNoValues() .dispose() }
Notice how we used the test() method on the Observable<Peripheral>
returned by scan()
. This allows us to subscribe to said Observable
in a testing capacity as a TestObserver. TestObserver
gives us the ability to introspect and perform assertions on the values emitted by the Observable
that was subscribed to, and is an extremely convenient way of unit testing Observable
s and their emissions.
We proceeded to run the test, and noticed that it failed with an error of “java.lang.RuntimeException: not implemented”, because at the time of writing, the default RxBleClientMock
object provided by the MockRxAndroidBle library hasn’t provided an implementation for the scanBleDevices(ScanSettings scanSettings, ScanFilter... scanFilters)
method. This is a prime example showing that we can sometimes be limited by what the MockRxAndroidBle artifact provides us with out of the box. Fret not, as this simply means that we’ll need to provide our own mocked version of RxBleClient
that does override and implement that method.
class MockRxBleClient(private val scanResults: Observable<ScanResult>) : RxBleClient() { override fun getBleDevice(macAddress: String): RxBleDevice { TODO("not implemented") } override fun getState(): State { TODO("not implemented") } override fun scanBleDevices(vararg filterServiceUUIDs: UUID?): Observable<RxBleScanResult> { TODO("not implemented") } override fun getBackgroundScanner(): BackgroundScanner { TODO("not implemented") } override fun getBondedDevices(): MutableSet<RxBleDevice> { TODO("not implemented") } override fun observeStateChanges(): Observable<State> { TODO("not implemented") } override fun scanBleDevices( scanSettings: ScanSettings, vararg scanFilters: ScanFilter ) = scanResults }
In the snippet above, we created our own mocked RxBleClient
, simply named MockRxBleClient
. For the sake of simplicity, we only overrode the scanBleDevices
function. You may need to override other functions to fit the use case that you wish to test. Notice how our MockRxBleClient
takes in an Observable<ScanResult>
as its constructor argument, and returns that same Observable
when scanBleDevices
is called. This essentially means that our test code now has full control over what scanBleDevices
will emit — we’ve successfully mocked the BLE scan behavior of our code!
Our focus now shifts to providing our MockRxBleClient
with a ScanResult
object matching our test requirements. Note that RxAndroidBle
’s ScanResult
object can be initialized directly, but it requires an instance of ScanRecord
(itself an interface) during initialization. This means that we need to mock ScanRecord
to produce the behavior we desire around having a manufacturer specific data with an identifier of 224, with the corresponding data being a UTF-8 encoded string representing the peripheral’s serial number:
class MockScanRecord( private val serialNumberBytes: ByteArray ) : ScanRecord { override fun getTxPowerLevel(): Int { TODO("not implemented") } override fun getServiceData(): MutableMap<ParcelUuid, ByteArray> { TODO("not implemented") } override fun getServiceData(serviceDataUuid: ParcelUuid?): ByteArray? { TODO("not implemented") } override fun getAdvertiseFlags(): Int { TODO("not implemented") } override fun getBytes(): ByteArray { TODO("not implemented") } override fun getServiceUuids(): MutableList<ParcelUuid> { TODO("not implemented") } override fun getDeviceName(): String? { TODO("not implemented") } override fun getManufacturerSpecificData() = SparseArray<ByteArray>(1).apply { append(224, serialNumberBytes) } override fun getManufacturerSpecificData(manufacturerId: Int) = if (manufacturerId == 224) { serialNumberBytes } else { null } }
Once again, we only overrode the functions that are of interest to our unit test; your needs may vary. Once ScanRecord
can be mocked, we can finally circle back and write a full unit test. Recall that the goal of the test is to ensure that a Peripheral
object that’s surfaced as a scan result indeed contains the manufacturer specific data parsed as a UTF-8 string, with this value assigned as its serial number.
In the snippet below, pay attention to the setting up of test expectations near the beginning of the test function, and how we then proceeded to set up the mocks based on these expectations. Finally, we injected the test target BluetoothManager
with the mocked RxBleClient
, performed a scan, and asserted that the emitted Peripheral
’s serial number matches our test expectations.
@Test fun scan_returnsPeripherals_withSerialNumbersParsed() { val context = ApplicationProvider.getApplicationContext<Context>() val expectedSerialNumber = "01234567" val expectedSerialNumberBytes = expectedSerialNumber.toByteArray(Charsets.UTF_8) val mockDevice = RxBleClientMock.DeviceBuilder() .deviceMacAddress("00:11:22:33:44:55") .deviceName("Test Device") .scanRecord(byteArrayOf()) .rssi(-50) .build() val mockClient = MockRxBleClient( Observable.just( ScanResult( mockDevice, -50, 0L, ScanCallbackType.CALLBACK_TYPE_ALL_MATCHES, MockScanRecord(expectedSerialNumberBytes) ) ) ) val bluetoothManager = BluetoothManager(context, mockClient) bluetoothManager .scan() .test() .assertNoErrors() .assertValueCount(1) .assertValue { it.serialNumber == expectedSerialNumber } .dispose() }
Tips on Testing Other Common BLE Use Cases
Aside from testing the parsing of meaningful data from advertisement packets, other common BLE use cases that are prime candidates for unit testing include:
- Making sure the
BluetoothManager
(or equivalent) handles and parses incoming notifications and indications appropriately. - Making sure both success and error outcomes for BLE operations such as reads and writes concerning characteristics and descriptors are handled.
- If implemented, making sure the correct
Exception
s are surfaced for scenarios where a BLE operation is requested when BLE is off or not available.
For testing incoming notifications and indications, RxBleClientMock.DeviceBuilder
’s notificationSource()
method plays a crucial role in enabling the test code to control when a notification or indication happens, and what data comes in with it.
If you ever need to create an instance of a class whose constructor is private, or whose dependencies required in its constructor are too tedious to generate, creating a stubbed mock instance of that class using the Mockito framework may be the way to go. For instance, BluetoothGatt
cannot normally be instantiated by client code, but we’re able to generate a BluetoothGatt
and control what its getDevice()
method returns by using Mockito like so:
import org.mockito.Mockito.`when` as whenever // ... val gatt = Mockito.mock(BluetoothGatt::class.java) val device = Mockito.mock(BluetoothDevice::class.java) whenever(gatt.device).thenReturn(device)
RxAndroidBle makes it incredibly easy to unit test an Rx Android codebase that requires BLE functionalities, because it handles most of the heavy lifting of mocking for developers. The thing to remember is that we’re not restricted by the ready-to-use mocks provided by MockRxAndroidBle. All the main objects in the library are interfaces, and this gives us the freedom to mock them up however we want. In our opinion, if your codebase already uses RxJava and you need a library that abstracts away BLE complexities, RxAndroidBle should definitely be your first choice.
Happy unit testing!