JavaScriptJest

Jestのdescribe.eachとit.eachで非同期のテストを大量に実行する

「AかつBのとき、Cが出力される」みたいな複数の変数が出てくる関数ってよくありますよね。 あれをそのままテストコードにすると、だいたいこんな感じになります。 処理が複雑になってくると、組み合わせのパターンをできるだけ増や […]

広告ここから
広告ここまで

「AかつBのとき、Cが出力される」みたいな複数の変数が出てくる関数ってよくありますよね。

const add = (a, b) => {
    return a + b
}

あれをそのままテストコードにすると、だいたいこんな感じになります。

describe('test', () => {
    it('1 + 2', () => {
        expect(add(1, 2)).toEqual(3)
    })
    it('1 + 1', () => {
        expect(add(1, 1)).toEqual(2)
    })
    it('2 + 2', () => {
        expect(add(2, 2)).toEqual(2)
    })
})

処理が複雑になってくると、組み合わせのパターンをできるだけ増やしたくなりますが、ちょっとDRYじゃないですしこれをいくつも書いてメンテナンスするのはちょっと辛いです。

describe / it / testにはeachメソッドがある

こういう似たようなテストが続く場合は、eachを使うと便利です。

describe('test', () => {
    it.each([
        [1, 1, 2],
        [1, 2, 3],
        [2, 2, 4],
        [2, 3, 5],
    ])("%i + %i = %i", (inputA, inputB, expectedResult) => {
        expect(add(inputA, inputB)).toEqual(expectedResult)
    })
})

テスト実行時にもちゃんと値が入ります。

  test
    ✓ 1 + 1 = 2
    ✓ 1 + 2 = 3
    ✓ 2 + 2 = 4 (1ms)
    ✓ 2 + 3 = 5

Promiseのresolve / rejectをテストする

ところでこの処理を非同期でやる場合はどうすればよいでしょうか。

適当な非同期処理を行うコードをまず用意します。

const isOdd = (num) => num % 2

/**
 * 偶数だけの足し算を実行する
 * @param {number} a 
 * @param {number} b 
 */
const addOddAsync = async(a, b) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (isOdd(a) && isOdd(b)) {
                resolve(a + b)
            } else {
                reject(new Error("All input must be odd"))
            }
        }, 1000)
    })
}

有用性については今は忘れましょう。引数に奇数を入れるとrejectされる非同期関数ができました。

これに対してテストを書いてみます。

describe('test', () => {
    it.each([
        [1, 1, 'resolve', 2],
        [1, 2, 'reject', 'All input must be odd'],
        [1, 3, 'resolve', 4],
        // これはコケる(rejectされるのにresolveをexpectしちゃってるため)
        [2, 2, 'resolve', 'All input must be odd'],
    ])("%i + %i should return %p (result: %p)", async (inputA, inputB, promiseResult, expectedResult) => {
        const task = addOddAsync(inputA, inputB)
        if (promiseResult === 'resolve') {
            await expect(task).resolves.toEqual(expectedResult)
        } else if (promiseResult === 'reject') {
            await expect(task).rejects.toThrowError(expectedResult)
        } else {
            throw new Error(`${promiseResult} does not supported`)
        }
    })
})

ちょっと雑ですが、eachの値に「resolveかrejectか」を追加してそれを元にアサーションを実行するようにしています。

このresolve / rejectを間違えてもエラーが出ますので、ミスにも気付きやすいかなと思います。

  ● test › 2 + 2 should return "resolve" (result: "All input must be odd")

    expect(received).resolves.toEqual()

    Received promise rejected instead of resolved
    Rejected to value: [Error: All input must be odd]

      33 |         const task = addOddAsync(inputA, inputB)
      34 |         if (promiseResult === 'resolve') {
    > 35 |             await expect(task).resolves.toEqual(expectedResult)
         |                   ^
      36 |         } else if (promiseResult === 'reject') {
      37 |             await expect(task).rejects.toThrowError(expectedResult)
      38 |         } else {

      at expect (node_modules/expect/build/index.js:138:15)
      at expect (__tests__/unit/lib/username.utils.js:35:19)

Describe.eachも使ってみる

describe側でもループを実行することで、よりテストケースのコード量を減らすこともできます。

const add = (a, b) => {
    return a + b
}    

   describe.each([
        1,
        2,
        3,
    ])("inputA is %i", (inputA) => {
        it.each([
            1,
            2,
            3
        ])("input B is %i", (inputB) => {
            expect(add(inputA, inputB)).toEqual(inputA + inputB)
        })
    })

この例だとあまり有用ではないですが、例えば「特定のドメインのときは常にtrue、だけどそれ以外の場合は何を投げてもrejectする」のようなバリデーションをかけるときなどに使うと便利です。

ブックマークや限定記事(予定)など

WP Kyotoサポーター募集中

WordPressやフロントエンドアプリのホスティング、Algolia・AWSなどのサービス利用料を支援する「WP Kyotoサポーター」を募集しています。
月額または年額の有料プランを契約すると、ブックマーク機能などのサポーター限定機能がご利用いただけます。

14日間のトライアルも用意しておりますので、「このサイトよく見るな」という方はぜひご検討ください。

広告ここから
広告ここまで

Related Category posts