[Jest] いずれかの値にマッチしたらパスするテストを書きたい(IoTシステムの結合試験)

2021.12.29

こんにちは、CX事業本部 IoT事業部の若槻です。

今回は、Jestでいずれかの値にマッチしたらパスするテストを書いてみました。

IoTデータの取得と集計が期待通り行われることをテストしたい

特定領域内にある物体をカウントおよび集計するIoTシステムを実装する機会がありました。カウントは約1分毎に行われますが、物体の出入りはさほど激しくはありません。そしてIoTデータとして取得されたカウント数データは、AWS上でLambda関数などにより属性ごとのカウント数に集計されて刻一刻とCloudWatchメトリクスに発行されます。

さて、AWSクラウド上に構築済みのこのIoTシステムの結合試験を行うテストコードを作成したいとなりました。そこで考えた方法としては、まずIoTデータ取得部分のAPIを直接叩き、取得したIoTデータを集計処理した値(期待値)を作成します。さらにCloudWatchメトリクスの最新のデータポイントをAPIを叩いて取得(実際値)し、両者をJestのマッチャーに掛けて比較しようというものです。

しかしカウント数は緩やかにですが刻一刻と変化します。またIoTデバイスが常に正確なデータを返すとは限りません。そのためテストを実行したタイミングによってはコードが正しくても期待値と実際値がずれる可能性があります。

そこで、実際値が期待値の+-1の範囲内に収まっていればOKと考え、期待値-1期待値期待値+1いずれかにマッチしていればパスするテストを作ることにしました。

方法はいくつかある

「いずれかにマッチするテスト」をJestでどう実現できるのか、方法をいくつか確認してみました。

toBeTruthy()

まず思いついたのはtoBeTruthy()です。

値がどのようなものかを気にせず、真偽値のコンテクストの中で値が真であることを確認したい場合は.toBeTruthy を使用して下さい。 例えば、以下のようなアプリケーションコードがあったとします:

これを使用すれば、expect()内の評価がtrueになればテストをパスとすることができます。

expect(response === 9 || response === 10 || response === 11).toBeTruthy();

toContain()

次にtoContain()です。

アイテムが配列内にあることを確認したい場合は、.toContain を使用します。 配列内のアイテムをテストするために、 このマッチャは===を使用して厳密な等価性のチェックを行います。 .toContainは、ある文字列が別の文字列の部分文字列であるかをチェックすることもできます。

期待値をリストで表してtoContainで実際値との評価をしています。やってること自体はexpect()と同じですが、記述がとてもすっきりしますね。

expect([9, 10, 11]).toContain(response);

試してみた

試しに次のようにテストコードを書いてみます。期待値は91011のいずれかです。

//実際値を取得する処理(実際にはGetMetricsData APIなどを叩く)
const getCurrentMetrics = (): number => {
  //9, 10, 11, 100のいずれかが取得される
  const array = [9, 10, 11, 100];
  return array[Math.floor(Math.random() * array.length)];
};

it('テスト - toBeTruthy', () => {
  const response = getCurrentMetrics();
  console.log(response);

  expect(response === 9 || response === 10 || response === 11).toBeTruthy();
});

it('テスト - toContain', () => {
  const response = getCurrentMetrics();
  console.log(response);

  expect([9, 10, 11]).toContain(response);
});

npx jestなどでテストを実行すると、いずれのパターンも成功しました。しかしテスト実行速度はtoContainのパターンの方が10倍近く速いですね!

$  npx jest
 PASS  test/sampleHandler.test.ts
  ✓ テスト - toBeTruthy (11 ms)
  ✓ テスト - toContain (1 ms)

  console.log
    9

      at Object.<anonymous> (test/sampleHandler.test.ts:10:11)

  console.log
    11

      at Object.<anonymous> (test/sampleHandler.test.ts:17:11)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.277 s
Ran all test suites.

期待値範囲外の値(100)が取得された場合は、ちゃんとテストは失敗します。

$  npx jest
 FAIL  test/sampleHandler.test.ts
  ✓ テスト - toBeTruthy (11 ms)
  ✕ テスト - toContain (3 ms)

  ● テスト - toContain

    expect(received).toContain(expected) // indexOf

    Expected value: 100
    Received array: [9, 10, 11]

      17 |   console.log(response);
      18 |
    > 19 |   expect([9, 10, 11]).toContain(response);
         |                       ^
      20 | });
      21 |

      at Object.<anonymous> (test/sampleHandler.test.ts:19:23)

  console.log
    10

      at Object.<anonymous> (test/sampleHandler.test.ts:10:11)

  console.log
    100

      at Object.<anonymous> (test/sampleHandler.test.ts:17:11)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        0.753 s, estimated 3 s
Ran all test suites.

今回はtoContain()による方法を採用

というわけで、今回は記述もシンプルで速度もあるtoContain()を採用してテストを作成しました。

しかしtoContain()はカウント数のような整数値(離散値)の確認には適していますが、気温などの小数を含む値(連続値)の場合はリストでは表せないため、toBeTruthy()で数値の範囲内外を評価するテストを作ることになるかと思います。ここはケースバイケースですね。

その他の方法

ぐぐってみると、Stack Overflowで同じような質問のエントリが見つかりましたが、その中では今回の2つの方法の他に、try/catchを使用した方法や、jestをextendする方法なども回答として上がっていました。「いずれかの値にマッチ」以外の評価をOR条件でテストしたい場合はこれらの方法になってきそうですね。

テストのリトライの実装も考えたい

Jestでは、jest-circusパッケージとjest.retryTimes()を使用することでテストのリトライを実装できるようです。

今回のような、いずれかにマッチするテストはリトライ処理も組み合わせると、想定外の値によりテストが失敗した際の手動リトライの手間もいくらか省けますね。(今回はCI/CDに組み込むなどしていない、全体のテスト量はさほど多くないためリトライは容易、などの理由により省きました。)

参考

以上