JestでTable Driven Testsをする

JestでTable Driven Testsをする方法を紹介します。Jestでは配列形式と、タグ付きテンプレートリテラル形式でテストを書けるので、2つの記述法を解説します。また、TypeScriptで書いた場合の型推論と、アサーションの方法についても紹介します。

2021/10/103 min read
..
hero image

はじめに

Table Driven Tests は主に Go lang で推奨されるテスト手法です。 入力と期待される結果を含む完全なテストケースをテーブルとして定義し、テスト対象に対してテストケースをイテレーションしてテストを行います。 つまり、テストスイートを 1 回だけ記述し、テストデータを渡すことができます。 テストを作成するときにコピーアンドペーストが多い場合、テストケースをテーブルにリファクタリングできる可能性が高いです。

ちなみに Go lang の公式では次のように述べられています。

Writing good tests is not trivial, but in many situations a lot of ground can be covered with table-driven tests

jest でも Table Driven Test がサポートされているので、その方法を共有したいと思います。

テストケースの書き方

jest では 2 つの書き方でテストケースを表現できます。テスト対象として条件分岐があるような次のケースを考えます。

index.html 以外の *.html を */index.html に変換する関数

1
2
3
4
5
6
7
8
9
10
11
import { dirname, join, parse } from 'path'
export const path2IndexHtml = (path: string): string => {
const EXT = '.html'
const INDEX = 'index'
const { ext, name, dir } = parse(path)
if(ext !== EXT) return path
if(name === INDEX) return path
return join(dir, name, `${INDEX}${EXT}`)
}
index.tsts

この関数自体は、Server Side Generation の実装 でファイルを生成する際に、ディレクトリを掘ってほしいときに使いました。レアケースですかね笑

テーブルの配列でテストする

1 つ目の書き方は、テーブルを配列として定義して渡す方法です。 次のように書きます。

1
2
3
describe.each(table)(name, fn, timeout)
it.each(table)(name, fn, timeout)
test.each(table)(name, fn, timeout)
ts

Alias があるのでいくつかのオブジェクトが each メソッドを持っています。 具体的なテストケースは次のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { path2IndexHtml } from '../src'
describe('path2IndexHtml', () => {
const table = [
['', ''],
['index.html', 'index.html'],
['/index.html', '/index.html'],
['index.css', 'index.css'],
['about.css', 'about.css'],
['about/index.css', 'about/index.css'],
['about.html', 'about/index.html'],
['hoge/about.html', 'hoge/about/index.html'],
['/hoge/about.html', '/hoge/about/index.html'],
['aindex.html', 'aindex/index.html'],
['indexa.html', 'indexa/index.html'],
['/about/index.html', '/about/index.html'],
]
it.each(table)('pattern1: path2IndexHtml(%s) = %s', (path, expected, fa) => {
expect(path2IndexHtml(path)).toBe(expected)
})
})
index.spec.tsts

テストケースを2次元配列で記述します。配列内の要素の順番はそのままに fn の引数として渡されます。 また、 name にはテストスイートのタイトルを指定します。 printf の書式に従うパラメータを注入することで、ユニークなテストタイトルを生成できます。 詳細はこちらを確認してください。 これも配列の要素順にパラメータが渡されます。

ちなみに 1 次元の配列を渡した場合には、内部的には [1, 2, 3] -> [[1],[2],[3]] のように変換されます。

また、fn に渡されるパラメーターは TypeScript の場合、型推論されます。 上の例では tablestring[][] 型なので、 fn の引数は ...args: string[] と推論されます。 タプルとして推論させる場合は as consttable につけるとうまく推論されます。

他にも each メソッドはジェネリックス型を受け入れるので、次のように型を指定できます。

1
it.each<string[]>(table)
index.spec.tsts

このテストを実行すると次の出力になりました。

1
2
3
4
5
6
7
8
9
10
11
12
13
path2IndexHtml
✓ path2IndexHtml() ->
✓ path2IndexHtml(index.html) -> index.html
✓ path2IndexHtml(/index.html) -> /index.html
✓ path2IndexHtml(index.css) -> index.css
✓ path2IndexHtml(about.css) -> about.css
✓ path2IndexHtml(about/index.css) -> about/index.css (1 ms)
✓ path2IndexHtml(about.html) -> about/index.html
✓ path2IndexHtml(hoge/about.html) -> hoge/about/index.html
✓ path2IndexHtml(/hoge/about.html) -> /hoge/about/index.html
✓ path2IndexHtml(aindex.html) -> aindex/index.html
✓ path2IndexHtml(indexa.html) -> indexa/index.html
✓ path2IndexHtml(/about/index.html) -> /about/index.html
bash

パラメーターがテストタイトルに埋め込まれてます。テストスイートを最小限に、様々なパラメーターのテストができました。

タグ付きテンプレートリテラルでテストする

タグ付きテンプレートリテラルでテーブルを表現することもできます。

インターフェイスは次のようになります。

1
2
3
4
5
6
7
8
9
describe.each`
table
`(name, fn, timeout)
it.each`
table
`(name, fn, timeout)
test.each`
table
`(name, fn, timeout)
ts

実際に上の例と同じテストを書くと、次のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
describe('path2IndexHtml', () => {
it.each`
path | expected
${''} | ${''}
${'index.html'} | ${'index.html'}
${'/index.html'} | ${'/index.html'}
${'about.css'} | ${'about.css'}
${'about/index.css'} | ${'about/index.css'}
${'about.html'} | ${'about/index.html'}
${'hoge/about.html'} | ${'hoge/about/index.html'}
`('path2IndexHtml($path) -> $expected', ({ path, expected }) => {
expect(path2IndexHtml(path)).toBe(expected)
})
})
index.spec.tsts

table の 1 行目は変数名を指定します。後続の行は ${value} 構文でテストケースを記述します。 string 型でも ${} で囲わなければなりません。

fn の引数にはオブジェクトの形式で渡されるので、分割代入で受け取るといいと思います。

name のテストタイトルにパラメーターを使う場合は $name 形式で変数にアクセスできます。

この記法だと、テーブルの形でテストケースを記述できる点が利点です。 しかし、 string の多いテストケースの場合は ${} と クオートであまり見やすくはありませんね。

また、この記法では fn の引数の型推論が any 型になってしまいます。 タグ付きテンプレートリテラルなので、ジェネリクスを受け入れられないため、これは仕方がありません。

どうしても型をつけたい場合は、fn 関数に型を定義します。

1
2
3
4
'path2IndexHtml($path) -> $expected',
({ path, expected }: { path: string; expected: string }) => {
expect(path2IndexHtml(path)).toBe(expected)
}
index.spec.tsts

結果はどちらも同じになるので、テストケースや好みで記法を使い分けるといいと思います。


Edit this page on GitHub

Other Article

Comments