🤡

フロントのJestでグローバルなメソッドが落ちる事象とその対処

2023/07/07に公開

画像ライブラリを使用したコンポーネントのテストでハマったため覚書

発端

Next.jsのプロジェクトで、こちらの画像をズームするモジュール(react-medium-image-zoom)を使用したコンポーネントを作成。
その後、それの単体テストを書いたところボコボコに落ちました。

エラー内容(抜粋)

test ExampleMocked › When passing a local image mocked path, it should be displayed.

    TypeError: img.decode is not a function

       8 |       "https://github.blog/wp-content/uploads/2023/05/1200.630-Community-wLogo.png";
       9 |
    > 10 |     render(<Example imageUrl={imageUrl} />);
         |     ^
      11 |     expect(screen.getByRole("img")).toBeInTheDocument();
      12 |   });
      13 | });

作成したコンポーネントのコード

https://github.com/amkkr/next-experiment-note/blob/88e394e0f33d09a414805f1240a633376786b529/src/components/image/Example.tsx#L1-L29

エラーが出たテストコード

https://github.com/amkkr/next-experiment-note/blob/ca4fb612793e8348e88bc7698666a6000ef0177a/src/components/image/__tests__/Example.test.tsx#L1-L21

原因

画像モジュールが内部でグローバルスコープに属するDOM APIHTMLImageElement: decode()を叩いていて、こちらをjsdomがサポートしていないために解決できないままテストが落ちるという話でした。

対処方法

解決策は自前でモックしてあげることです。
1個ずつ出てきたエラーをモックするのですが、このモックしたい(落ちる)メソッドを持っているHTMLImageElementはGlobalに存在するため、明示的にimportしてクラスモックして..ということもできません。
そのため、jestのmockとは別のやり方をします。該当のモックがこちら。
https://github.com/amkkr/next-experiment-note/blob/4e44c6321751b46e640215758416f4ea90b8644c/test/config/setup-jest.ts#L1-L17

何をしているかというと、Object.defineProperty() を使い、テスト時のみモックしたいdecode()を直接再定義してテスト用にPromiseを返すだけのメソッドにしています。
ここでPromiseを返すようにしている理由は、decode()メソッドの定義が以下のため、これを再現するためです。

DOM に画像を追加しても安全になったときに解決されるプロミス (Promise) を返します。
引用元: https://developer.mozilla.org/ja/docs/Web/API/HTMLImageElement/decode

上記のような内容のbeforeAll()を各テストファイルに記述するか、1つのファイルに記述してjest.config.tsなどにsetupFilesAfterEnv: ["<rootDir>/test/config/${your-jest-config-file-name}.ts"],設定してあげればテストにPASSするようになります。

最後に

もともと、仕事で単体テスト書くぞとやってみたら落ちたところが発端でしたが、一応動く形にはなりました。(今回の記事と、動作検証で用意したコードのためここでは画像の取得が失敗した時などのケースは網羅していません)
また、jsdomがサポートしていない且つglobalなオブジェクトが原因でテストが落ちた時に、どうモックしていくかというのは多少なりとも知見を得られたと思っています。

ただ、この画像を扱うといった場合に、

  • Playwrightだとこんなモックをしなくともdomがちゃんとやってくれた
  • 画像という動作よりも見た目に大きな比重を占めるコンポーネントの場合に、モジュールが呼び出せるか以上の単体テストの意味はなく、より現物に近いインテグレーションテストで確認するべきだったのか
  • https://testingjavascript.com/ こういう話もあるし、UnitTestより上のテストに任せるべきだった?

などは考えてしまいますが、最適解は自分はまだ見つけられていません。
最後に少々脱線しましたが、いい勉強になりました。

Discussion