JestでDateのコンストラクタをモックする

下記の関数があるとしましょう。

1
2
3
function calcDuration(date: string) {
return Math.abs(new Date().getFullYear() - new Date(date).getFullYear());
}

この関数のテストを書くのに、注意しなければいけないことが1点あります。もう気づいたと思いますが、new Date()の結果を固定値にしないと、テストの結果が実行時の日付に依存してしまいますので、環境依存が発生し許容できないでしょう。Jestにはモジュールモックを含め、いくつのやり方がありますが、標準ビルトインオブジェクトかつコンストラクタの場合、どうモックするか、ドキュメントには詳しく書かれませんでした。ウェブで検索してみた結果jest.spyOn(global, 'Date')という感じで書くのは一番直感的で柔軟性が高いと感じました。よって、テストのコードはこうなるかと思います。

1
2
3
4
5
6
test("mock", () => {
const dateToUse = new Date("2015-01-01");
jest.spyOn(global, "Date").mockImplementation(() => dateToUse);
const duration = calcDuration("2011-01-01");
expect(duration).toBe(4);
})

しかし、上記のコードの実行結果は[Jest] Expected value to be (using Object.is): 4, Received: 0というエラーになるでしょう。理由は簡単です。calcDuration関数内でDateコンストラクタは二回も呼ばれ、そして常にdateToUseが返ってきます。よって、差分が0になるのは正しい振る舞いです。そこを修正する為に、mockImplementationにて条件分岐を作ればシンプルに解決できます。引数が空の場合dateToUseを返し、それ以外は元々のビルドインオブジェクトを使わせます。結果は下記の感じです。

1
2
3
4
5
6
7
8
9
10
test("mock", () => {
const OriginalDate = Date;
const dateToUse = new Date("2015-01-01");
const mocked = jest.spyOn(global, "Date").mockImplementation((arg) => {
return arg ? new OriginalDate(arg) : dateToUse;
});
const duration = calcDuration("2011-01-01");
expect(mocked).toHaveBeenCalledTimes(2);
expect(duration).toBe(4);
});

他にmockImplementationOnceでやる方法も考えられますが、実装を意識しないと書けなさそうと思いましたので、上記のやり方を採用することにしました。