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

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

# web-bluetooth-mockのインストール
WebBluetooth向けのMockモジュール[web-bluetooth-mock](https://github.com/urish/web-bluetooth-mock)は、urishさんがOSSで提供してくれているので、今回はこちらを利用します。

```bash
yarn add --dev web-bluetooth-mock
```

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

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

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

```TypeScript
// 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が呼ばれたかをチェック
`charMock`の`addEventListener`を`jest.spyOn`します。
これで、もし、テスト対象モジュールが、`charMock.addEventListener`をコールしたら、`expect(spy).toHaveBeenCalledWith`で引数の`type`含めて、評価可能です。なお、ここでは、`listner`関数の中身はドントケアにしています。

```TypeScript
  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`もチェック出来ます。

```TypeScript
  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`に、以下の様にデータ配列を渡してください。

```TypeScript
  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()`があれば、これがコールされるまでテストの終了を待ってくれます。

```TypeScript
  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させる
異常系もテストしておきたいですよね。以下は、`getCharacteristic`で`Promise.reject`させた例です。

```TypeScript
  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系の実装例等は、[ここ](https://github.com/urish/muse-js/blob/master/src/muse.spec.ts)などにありますが、少し古いのかそのままではTSエラーが出るものも有ります。適宜上記で読み替えてもらえると良いと思います。

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