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する」のようなバリデーションをかけるときなどに使うと便利です。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark