2019-12-10

TS と Jest で Table Driven Test をする

この記事はTypeScript Advent Calendar 2019の11日目の記事です。

Go を齧ったことのある方は馴染み深いと思うのですが、Table Driven Testing という手法があります。いきなり見せます:

const table = [
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
];

test.each(table)('.add(%i, %i)', (a, b, expected) => {
  expect(a + b).toBe(expected);
});

こんな感じで「テストケースひとつひとつを test(...) 等と記述するのではなく、引数と予期する結果を配列として持っておいてテストコードの共通化を図る」手法です。同じ関数に関して書かなければならないテストケースが多い場合に、ごちゃごちゃしがちなテストコードをすっきりさせることができると思っています。

はてさてこれも Go 病なのか、自分はどうしてもこう書きたくなってしまいます。

const table = [
  {
    name: '1+1',
    a: 1,
    b: 1,
    expected: 2,
  },
  {
    name: '1+2',
    a: 1,
    b: 2,
    expected: 3
  },
  {
    name: '2+1',
    a: 2,
    b: 1,
    expected: 3,
  },
];

// 動きません
test.each(table)(name, (a, b, expected) => {
  expect(a + b).toBe(expected);
});

テーブルを「オブジェクトの配列」にする形式ですね。今までパッと見 [1, 1, 2] がそれぞれ何を表すのかよくわからなかったのが、少しはわかりやすくなったのではないでしょうか。name フィールドでテスト名を明示するのは今回の例だとやり過ぎな感じもしますが、ちゃんとした振る舞いのテストであればテスト名には「何のためのテストか」という情報を含めなければなりませんから、これもテーブルに含めるのは自然と言えます。

しかし上記のコードは動きません。なぜなら Jest の test.each() の引数の型は unknown[][] だからです。つまり冒頭のような「配列の配列」の形式しか受け付けてくれないわけですね。

じゃあどうすれば良いでしょうか。「配列の配列」に変換してみます。

const table = [ ... ];
const arrayTable = table.map(row => Object.values(row)));

test.each(arrayTable)('%s', (_name, a, b, expected) => {
  expect(a + b).toBe(expected);
});

これで動くようになりました。わーい!!

…ちょっと待ってください。確かにテストはできましたが、上記のコードには TS 勢なら許せない部分があるのです。ありますよね?

そうです。 _name, a, b, expected の全てが any になっています。これはなんとかしなければなりません。

結論から言えば、 arrayTable をタプルの配列にすれば解決します。こんな感じです。

const table = [ ... ];
const arrayTable = table.map(row =>
  <[string, number, number, number]>[row.name, row.a, row.b, row.expected]
));

test.each(arrayTable)('%s', (_name, a, b, expected) => {
  expect(a + b).toBe(expected);
});

これで arrayTable の型は [string, number, number, number][] となり、 _name, a, b, expected にもそれぞれの型を割り当てられました。

ただ自分でタプルの型を書くのも微妙なので、こうしてみることにします。

// util func to create tuple.
const tuple = <T extends any[]>(...v: T) => v;

const table = [ ... ];
const arrayTable = table.map(row =>
  tuple(row.name, row.a, row.b, row.expected)
));

test.each(arrayTable)('%s', (_name, a, b, expected) => {
  expect(a + b).toBe(expected);
});

tuple() 関数は受け取った引数列をタプルとして返す関数です。TypeScript 3.0 で導入された Rest elements in tuple types を使っています。

以上です! これで今日からすっきりと Table Driven Test を書くことが出来ます! 😆

オチ

これでええやん

const table = [ ... ];
for (const { name, a, b, expected } of table) {
  test(name, () => {
    expect(a + b).toBe(expected);
  });
}

まとめ