🖼️

JestでURL.createObjectURLとloadイベントをモックする

3 min read

こういう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.createObjectURLloadイベントをモックしようとして少しハマったので記録を残しておきます。

Jestのバージョンは27.3.1、jest.config.js

jest.config.js
module.exports = {
  testEnvironment: "jsdom",
};

という環境です。

URL.createObjectURL

Jest 27.3.1の時点で依存で入るjsdomURL.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();
});

さらにwidthheightといった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,
    });
  });
});

これでめでたくテストは動き、正常にパスします。

脚注
  1. https://github.com/jsdom/jsdom/issues/1721 ↩︎

Discussion

ログインするとコメントできます