JESTでWebBluetoothを使ったTypeScriptコードをテストする


サマリ

TypeScriptで実装した、WebBluetoothを用いた自作ツールを、Mockモジュールを使ってJESTでテストするまでのご紹介です。
今までは、該当のデバイスと直接接続して手動でテストしていた人が多いかと思うのですが、これを使えばJESTの自動テストを導入できるので楽して品質向上できます。

web-bluetooth-mockのインストール

WebBluetooth向けのMockモジュールweb-bluetooth-mockは、urishさんがOSSで提供してくれているので、今回はこちらを利用します。

yarn add --dev web-bluetooth-mock

Testコードの準備

Characteristicにまつわるテストをするためには、だいたい以下のMockは使うと思いますので、テストコードの冒頭でimportしておきます。

import {
  DeviceMock,
  PrimaryServiceMock,
  CharacteristicMock,
  WebBluetoothMock,
} from 'web-bluetooth-mock';

JESTのコードを書く

テスト共通の処理

Characteristicのテストをするときは各テストを実施する前に実行される、共通の事前準備を実装しておくと楽です。Mockにテストデバイスの想定(各種初期設定)を与えておきます。
なお、最新のTSでは、下記のdeclare const globalany型としてNG食らってしまうので、ここだけ、ts-ignoreしています。もっと適切な方法はあるのかな?

// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
declare const global;

let deviceMock: DeviceMock;
let serviceMock: PrimaryServiceMock;
let charMock: CharacteristicMock;

const SERVICE_UUID = '10b20100-5b3b-4571-9508-cf3efcd7bbae';
const CHAR_UUID = '10b20108-5b3b-4571-9508-cf3efcd7bbae';

describe('CubeBatteryChar', (): void => {
  beforeEach(() => {
    // DeviceのMock. 名前とService UUIDを事前登録
    deviceMock = new DeviceMock('dev name', [SERVICE_UUID]);

    // Web Bluetoothのデバイスとして、登録
    global.navigator = global.navigator || {};
    global.navigator.bluetooth = new WebBluetoothMock([deviceMock]);

    // ServiceMockや、該当のCharのMockを準備
    serviceMock = deviceMock.getServiceMock(SERVICE_UUID);
    charMock = serviceMock.getCharacteristicMock(CHAR_UUID);
    charMock.value = new DataView(new Uint8Array([80/* Data */]).buffer);
  });

  test(....)
  //  中略
};

こんな感じで準備はOKなので、test関数を実装していきます。

個別のテストの実装例

例1. CharのaddEventListenerが呼ばれたかをチェック

charMockaddEventListenerjest.spyOnします。
これで、もし、テスト対象モジュールが、charMock.addEventListenerをコールしたら、expect(spy).toHaveBeenCalledWithで引数のtype含めて、評価可能です。なお、ここでは、listner関数の中身はドントケアにしています。

  test('addEventListener', async (): Promise<void> => {
    /* 接続処理などは省略 */

    const spy = jest.spyOn(charMock, 'addEventListener');
    await testTarget.hoge(); // addEventListener() is called here.
    expect(spy).toHaveBeenCalledWith('characteristicvaluechanged', expect.anything());
  });

例2. CharのstartNotificationsが呼ばれたかをチェック

例1とほぼ同様に、startNotificationsもチェック出来ます。

  test('startNotifications', async (): Promise<void> => {
    /* 接続処理などは省略 */

    const spy = jest.spyOn(charMock, 'startNotifications');
    await testTarget.bar(); // startNotifications() is called here.
    expect(spy).toHaveBeenCalled();
  });

例3. Charの値をRead

CharをReadした際に読み取ることができるデータ列を事前にMockへ設定することが出来ます。charMock.valueに、以下の様にデータ配列を渡してください。

  test('readValue', async (): Promise<void> => {
    /* 接続処理などは省略 */

    charMock.value = new DataView(new Uint8Array([33]).buffer);

    const spy = jest.spyOn(charMock, 'readValue');
    const result = await testTarget.read();

    // readValueが呼ばれたかをチェック
    expect(spy).toHaveBeenCalled();

    // 読みだして返って来た値が正しいかをチェック
    expect(result).toBe(33);
  });

例4. CharからNotifyを発行する

charMock.dispatchEventを使って、CharからのNotifyを偽装することが出来ます。その結果を受け取るべく、Test targetのEvent listnerとかで結果を確認する場合は、done()関数を導入しておかないと、テストが素通りしてパスしてしまうので、ご注意ください。done()があれば、これがコールされるまでテストの終了を待ってくれます。

  test('Notification', async (done): Promise<void> => {
    /* 接続処理などは省略 */

    // データを準備し、疑似的にNotifyを発行する。
    const testValue = 90;
    charMock.value = new DataView(new Uint8Array([testValue]).buffer);
    charMock.dispatchEvent(new CustomEvent('characteristicvaluechanged'));

    // 例えば、その結果を受け取るtest targetのevent listnerとかで結果を確認する。
    testTarget.addEventListener('change', (value: number): void => {
      // さっき準備したデータが上がってきたかを確認。
      expect(value).toBe(testValue);
      done();
      // ここまで来て、初めてTestがPassする。タイムアウトするとFail.
    });
  });

例5. getCharacteristicでPromise rejectさせる

異常系もテストしておきたいですよね。以下は、getCharacteristicPromise.rejectさせた例です。

  test('Prepare: Char error', async (): Promise<void> => {
    /* 接続処理などは省略 */

    serviceMock.getCharacteristic = jest.fn(() => {
      return Promise.reject(new Error('test'));
    });
    await expect(testTarget.foo()).rejects.toMatchObject(new Error('test'));
  });

たとえば、readValue等でも、同様にreject可能です。

その他の例

他にも、Write系の実装例等は、ここなどにありますが、少し古いのかそのままではTSエラーが出るものも有ります。適宜上記で読み替えてもらえると良いと思います。

それでは。快適なWebBluetooth/JESTをお楽しみください。