Node.js + Jest で async な関数を Mock しようとしてハマった

2020.10.11

症状

以下の状況下で、意図したようにモックが作成/適用されない。

  • テスト対象が async 関数で複数テストを行う時
  • それぞれのテストで異なるモックを使いたい時

例えば以下のような時に、Case1, Case2 でそれぞれモックを定義しているにもかかわらず、Case2 のモックが使用される問題。

class MockTarget {
    public async func(str: string): Promise<string> {
        let editedStr: string = str;        
        /* do something */
        return editedStr;
    }
}

class UsingMockTarget {
    public async func(str: string): Promise<string> {
        const mockTarget: MockTarget = new MockTarget();
        return await mockTarget.func(str);        
    }
}
jest.mock('./MockTarget'); // パスを指定
const Mock: jest.Mock = MockTarget as unknown as jest.Mock; // TypeScriptでは型変換する必要がある
test('Case1', async () => {
    /* Mock 生成 */
    Mock.mockReset();
    Mock.mockImplementation(() => {
        return {
            func: async (str:string) => {
                if (str == "test") {
                    return "test";
                }
                else {
                    return "";
                }
            }
        };
    });

    /* Test実行 */
    const usingMockTarget: UsingMockTarget = new UsingMockTarget();
    return expect(usingMockTarget.func("test")).resolves.toMatch("test");
});

test('Case2', async () => {
    /* Mock 生成 */
    Mock.mockReset();
    Mock.mockImplementation(() => {
        return {
            func: async(str:string) => {
                if (str == "err") {
                    throw new Error();
                }
                else {
                    return "";
                }
            }
        };
    });

    /* Test実行 */
    const usingMockTarget: UsingMockTarget = new UsingMockTarget();
    return expect(usingMockTarget.func("err")).rejects.toThrow();
});

原因

単純な話で、 await によって非同期的にテストが進んでしまうがために、 Case1 のテストが終了する前に Case2 のテストが開始されてモックが上書きされてしまう。

test自体をawait で駆動できれば良いのだが、無理っぽい。少し触った限りでは async 関数のテストをする時には Mock を入れ替えるのは難しそう。

全テストで利用可能な Mock を作らざる終えないが、込み入ったテストをする際に不自由しそう。何か解決策があるのだろうか?

jest.mock('./MockTarget'); // パスを指定
const Mock: jest.Mock = MockTarget as unknown as jest.Mock; // TypeScriptでは型変換する必要がある

/* 全てのテストで利用できるような汎用的なモック */
Mock.mockImplementation(() => {
    return {
        func: async (str:string) => {
            if (str == "test") {
                return "test";
            }
            else if (str == "err") {
                throw new Error();
            }
            else {
                return "";
            }
        }
    };
});

test('Case1', async () => {
    /* Test実実行 */
    const usingMockTarget: UsingMockTarget = new UsingMockTarget();
    return expect(usingMockTarget.func("test")).resolves.toMatch("test");
});

test('Case2', async () => {
    /* Test実実行 */
    const usingMockTarget: UsingMockTarget = new UsingMockTarget();
    return expect(usingMockTarget.func("err")).rejects.toThrow();
});