すべての組み合わせのテストコードはflatMapとmapで作成する

すべての組み合わせをflatMapとmapで作成

VitestやJestで「すべての組み合わせ」をテストしたいとき、各パターンを手作業で並べて書いているコードを見かけることがあります。

このようなコードは、手動で書いていると漏れや記述ミスが発生しやすくなります。

すべての組み合わせのテストコードはJavaScriptのflatMapとmapを組み合わせることで手作業で1つずつ書かずに自動生成できます。

組み合わせテストでよくある悩み

例えば、2つの引数を取る関数に対して複数の境界値(-1, 0, 1)を掛け合わせた全パターン(3×3 = 9通り)をテストしたい場合を考えます。

配列をそのまま並べてテストを書く場合、以下のような問題が生じます。

  • 手動で並べると要素が増えたときに記述量が増え、組み合わせの漏れに気付きにくい。
  • forEachをネストするとVitest (Jest)のtest.for (test.each) 構造から外れてしまい、個別テストとしてのログ可視性が落ちる。
sum.ts
export function sum(a: number, b: number) {
  return a + b
}
sum.spec.ts
// 以下のように書くと、組み合わせに漏れがあっても気づきにくい
import { expect, test } from 'vitest'
import { sum } from '@/sum'

const cases = [
  [-1, -1, -2],
  [-1, 0, -1],
  [-1, 1, 0],
  [0, -1, -1],
  [0, 0, 0],
  [0, 1, 1],
  [1, -1, 0],
  [1, 0, 1],
  [1, 1, 2],
]

test.for(cases)('sum(%i, %i) -> %i', (a, b, expected) => {
  expect(sum(a, b)).toBe(expected)
})

テストコードをflatMapとmapで自動生成する

flatMapとmapを組み合わせることで、複数の配列から「すべての組み合わせ」を持ったテスト用オブジェクト配列を1行で構築できます。

sum.spec.ts
import { expect, test } from 'vitest'
import { sum } from '@/sum'

const values = [-1, 0, 1]

// すべての組み合わせ (3 × 3 = 9パターン)
const cases = values.flatMap((a) =>
  values.map((b) => ({
    a,
    b,
    expected: a + b,
  }))
)

test.for(cases)('sum($a, $b) -> $expected', ({ a, b, expected }) => {
  expect(sum(a, b)).toBe(expected)
})

この書き方であれば、3つ以上の組み合わせも簡単に書けます。

mode.spec.ts
const types = ['A', 'B']
const statuses = ['active', 'inactive']
const flags = [true, false]

// 2 × 2 × 2 = 8パターンのテストデータが生成される
const cases = types.flatMap((type) =>
  statuses.flatMap((status) =>
    flags.map((flag) => ({
      type,
      status,
      flag,
    }))
  )
)

test.for(cases)(
  'type: $type, status: $status, flag: $flag',
  ({ type, status, flag }) => {
    // 中略
  }
)

すべての組み合わせテストのパターン作成が必要になったら、flatMapとmapを使用したパターンを活用してみてください。