🖼️
JestでURL.createObjectURLとloadイベントをモックする
こういうimageSize()
関数のテストを書きたいと思います:
imageSize.ts
type Size = {
width: number;
height: number;
};
const imageSize = (file: File): Promise<Size> => {
return new Promise((resolve, reject) => {
const img = document.createElement("img");
img.onload = () => {
resolve({
width: img.width,
height: img.height,
});
};
img.src = URL.createObjectURL(file);
});
};
export default imageSize;
ここでURL.createObjectURL
とload
イベントをモックしようとして少しハマったので記録を残しておきます。
Jestのバージョンは27.3.1、jest.config.js
は
jest.config.js
module.exports = {
testEnvironment: "jsdom",
};
という環境です。
URL.createObjectURL
Jest 27.3.1の時点で依存で入るjsdom
はURL.createObjectURL
を実装していません[1]。
なので何もモックせずにテストを実行すれば
TypeError: URL.createObjectURL is not a function
というエラーになりますし、そもそもURL.createObjectURL
が存在しないので
jest.spyOn(URL, "createObjectURL").mockImplementation(() => "");
のようにjest.spyOn()
しようとしても
Cannot spy the createObjectURL property because it is not a function; undefined given instead
というエラーになってしまいます。
なので直接jest.fn()
を代入してやる必要があります:
imageSize.test.ts
beforeEach(() => {
URL.createObjectURL = jest.fn();
});
afterEach(() => {
// @ts-ignore: URL.createObjectURL is mocked within beforeEach()
URL.createObjectURL.mockReset();
});
load
イベント
URL.createObjectURL
をモックしたので当然ながら画像のload
イベントは発火しません。
そこでdocument.createElement
をモックして、一定時間後にdispatchEvent()
することで擬似的にload
イベントを発火させるようにします:
imageSize.test.ts
beforeEach(() => {
// 先に元々の実装のdocument.createElement()を使ってimageElementを作っておく
const imageElement = document.createElement("img");
jest.spyOn(document, "createElement").mockImplementation(() => {
setTimeout(() => {
imageElement.dispatchEvent(new Event("load"));
}, 50);
return imageElement;
});
});
afterEach(() => {
jest.restoreAllMocks();
});
さらにwidth
やheight
といったattributeを設定しておくことで、その値をテスト本体で使うこともできます:
imageSize.test.ts
const imageElement = document.createElement("img");
jest.spyOn(document, "createElement").mockImplementation(() => {
+ imageElement.setAttribute("width", "640");
+ imageElement.setAttribute("height", "480");
+
setTimeout(() => {
imageElement.dispatchEvent(new Event("load"));
}, 50);
まとめ
テストコードの全体はこんな感じになりました:
imageSize.test.ts
import imageSize from "./imageSize";
describe("imageSize", () => {
const mockedImageWidth = 640;
const mockedImageHeight = 480;
beforeEach(() => {
URL.createObjectURL = jest.fn();
// 先に元々の実装のdocument.createElement()を使ってimageElementを作っておく
const imageElement = document.createElement("img");
jest.spyOn(document, "createElement").mockImplementation(() => {
imageElement.setAttribute("width", mockedImageWidth.toString());
imageElement.setAttribute("height", mockedImageHeight.toString());
setTimeout(() => {
imageElement.dispatchEvent(new Event("load"));
}, 50);
return imageElement;
});
});
afterEach(() => {
// @ts-ignore: URL.createObjectURL is mocked within beforeEach()
URL.createObjectURL.mockReset();
jest.restoreAllMocks();
});
it("画像のwidthとheightが返る", async () => {
const pngFile = new File([""], "test.png");
const size = await imageSize(pngFile);
expect(size).toEqual({
width: mockedImageWidth,
height: mockedImageHeight,
});
});
});
これでめでたくテストは動き、正常にパスします。
Discussion