サマリ
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 global
はany
型として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が呼ばれたかをチェック
charMock
のaddEventListener
をjest.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させる
異常系もテストしておきたいですよね。以下は、getCharacteristic
でPromise.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をお楽しみください。