Mocking Bluetooth / BLE traffic for fast robust app UI testing

Published: 2021-06-24 by Lars  testtools

In this blog post I announce a new tool for mocking Bluetooth Low Energy (BLE) traffic in unit tests. This will allow developers to do integration testing of mobile apps for Bluetooth devices with the speed and robustness of ordinary unit tests with 10s or 100s of tests per second.

The tool's target audience will have some existing experience with automated testing of mobile apps. You are probably already frustrated by the slowness and fragility of ordinary end-to-end testing, and equally frustrated by manual mocks in unit tests frequently becoming outdated.

Background

I help teams build software products using continuous delivery. Having adequate automated test coverage is essential to making continuous delivery work in practice, and making testing fast is essential for developer productivity. You can find my earlier blog posts and talks about mock recording here:

Developers utilize mock recording when they run their UI tests with pre-recorded mocks from actual interactions between UI and external services or devices. This contrasts manual mocking where developers write and maintain the code to mock the interactions manually.

After working primarily on web-based products, lately I have been working on a mobile app with a company producing a Bluetooth-enabled device. While good tools exist for mock recording of HTTP traffic (such as PollyJS and Hoverfly), we were not able to find an adequate solution for BLE traffic. Based on my experience with the HTTP-based mock recording tools, I built a tool for mock recording of BLE traffic in React Native applications.

This tool enables full integration testing of the React Native app and the Bluetooth device. And at the same time, the tests perform several orders of magnitude better than ordinary end-to-end testing.

Goals

The tool is meant for mobile app developers. I want the developer experience to be great: tests should be easy to write, fast to run, and the tool itself should integrate well into both normal development workflows and continuous integration pipelines.

The tool should allow developers to record BLE traffic from the app running on a real phone interacting with a real device. These recording files can then be used during normal development to mock BLE traffic when running app tests.

You can speed up testing by replacing almost all of your current end-to-end tests with unit tests and mock recording. In addition, you probably still want a few end-to-end tests for smoke testing purposes.

Please note that you will write separate tests for recording and for app testing. I have not attempted to design the tool in a way where the same test can be used for both purposes.

Demo

Take a look at this sample project, which shows the tool in action. The project is a standard React Native project, using the react-native-ble-plx library for BLE communication and Jest for testing.

The app will show a list of nearby Bluetooth devices, and when the user presses an item in the list, it shows the battery and signal level of the device, like this:

Here is an excerpt from the test of the DeviceList component of the app:

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

    // when: render the app
    const { getByA11yLabel, queryByA11yLabel } = render(
      withStore(<DeviceListScreen />, configureStore())
    );

    // then: no loading indicator is shown
    expect(queryByA11yLabel('Connecting to "The Speaker"')).toBeFalsy();

    // 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: loading indicator is shown
    expect(queryByA11yLabel('Connecting to "The Speaker"')).toBeTruthy();

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

    // then: eventually signal strength is shown
    expect(getByA11yLabel('"The Speaker" signal')).toHaveTextContent("📶 -42");

    // finally
    blePlayer.expectFullCoverage();
  });
});

This test runs in less than a second, as you can see from this output.

$ npm test

> [email protected] test
> jest

 PASS  src/view/DeviceList.test.js
  DeviceList
    √ should load and show device info (599 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.946 s, estimated 11 s
Ran all test suites.

This test uses a recording file. Note how we use blePlayer.playUntil in the test above in order to reach that label in the recording.

The recording file will be generated by running the recording app, which contains steps like this:

it("should read battery level", async () => {
  const { id } = device;
  const services = await bleManager.servicesForDevice(id);
  expect(
    services.find(
      (s) => s.uuid.toLowerCase() === service.battery.uuid.toLowerCase()
    )
  ).to.exist;
  bleRecorder.queueRecordValue(base64FromUint8(42));
  const { value } = await bleManager.readCharacteristicForDevice(
    id,
    service.battery.uuid,
    characteristic.batteryLevel.uuid
  );
  const batteryLevel = uint8FromBase64(value);
  console.log(`(actual batteryLevel = ${batteryLevel})`);
  expect(batteryLevel).to.be.at.least(0);
  expect(batteryLevel).to.be.at.most(100);
});

The phone will display recording progress while recording:

Conclusion

I hope this presentation caught your interest in this technique of speeding up the process of automated testing for React Native apps and Bluetooth devices. Go ahead and try it out for yourself and build your own demo. Enjoy!

Discuss on Twitter