Mock recording for Bluetooth Low Energy (BLE)

Mock recording for BLE traffic is useful when testing a mobile app used for controlling a physical device over Bluetooth/BLE. When using React Native to build the app, a popular library for interfacing with BLE is react-native-ble-plx. A mock recording tool for this library was recently introduced at SOUNDBOKS and is available as react-native-ble-plx-mock-recorder.

Real world experience

This clip shows the speed of running the same connectToDevice test 10 times over. This Jest test verifies the full app code for connecting to a SOUNDBOKS speaker over BLE, with around 50 BLE messages being exchanged. The test takes less than 200ms.

SOUNDBOKS app connect to device test GIF

Diagram

diagram of mock recording for BLE

When your app is running normally on the phone, it uses react-native-ble-plx to communicate over Bluetooth BLE with devices.

When testing the app with this tool, the react-native-ble-plx module is automatically mocked with a version that plays back traffic from a recording. This mock implements the same interface as the original module, plus a few methods, so that in your test, you can specify which recording to use, when to playback events, and optionally verify that the entire recording has been used when a test is complete. In this way you can use Jest and Testing Library like normally to test components and services that interacts with the device.

When you write scenarios for the recorder app, you will use a version of the react-native-ble-plx module wrapped in a recorder, so that all commands and events are not only propagated to and from the original module, but also persisted in a recording file. The wrapper implements the same interface as the original module, plus a few methods so your recording scenarios can insert labels into recordings. The recorder app can run through a number of scenarios, and create recordings for each.

Sample code

We can record BLE traffic using react-native-ble-plx-mock-recorder like this

describe("app", () => {
it("should receive scan results", async () => {
const bleRecorder = new BleRecorder({ bleManager: new BleManager() });
const { bleManagerSpy: bleManager } = bleRecorder;
await new Promise((resolve, reject) => {
bleManager.startDeviceScan(null, null, (error, { localName }) => {
if (!error && localName == expectedLocalName) {
resolve(d);
} else if (error) {
reject(error);
}
});
});
bleRecorder.label("scanned");
bleRecorder.close();
});
});

Then in our app test we can mock all BLE traffic with the recording:

describe("DeviceList", () => {
it("should load and show device info", async () => {
const recording = JSON.parse(
fs.readFileSync("../recorder/artifact/deviceList.recording.json")
);
const { blePlayer } = getBleManager();
blePlayer.mockWith(recording);

// when: render the app
render(withStore(<DeviceListScreen />, configureStore()));

// when: simulating BLE scan response
act(() => {
blePlayer.playUntil("scanned"); // Note: causes re-render, so act() is needed
});

// when: clicking a device
fireEvent.press(getByA11yLabel('Connect to "The Speaker"'));

// then: eventually battery level is shown
expect(getByA11yLabel('"The Speaker" battery level')).toHaveTextContent(
"🔋 42%"
);
});
});

Talk & blog post

More details can be found here: