node v18 で導入された global fetch を jsdom 環境下の jest で利用する方法

## 背景
- Node.js v18 で global fetch API が実験的に導入された
  - https://nodejs.org/en/blog/announcements/v18-release-announce#fetch-experimental
- これにより jest 等 Node.js のランタイムで fetch API を利用する際に node-fetch 等の外部ライブラリを導入する必要がなくなった
- しかし通常 React のテスト等で用いる jsdom 環境で利用するにはいくつか苦労があったのでまとめておく

## jest のバージョンを確認
Node.js のバージョンが v18 以降であっても jest のバージョンが v28 未満だと fetch API を利用できない。
https://jestjs.io/blog/2022/04/25/jest-28#all-nodejs-globals

package.json で jest のバージョンを `^28` 以上に指定して `npm install` または `yarn install` を実行することで、期待するバージョンの jest をインストールすることができる。

## jsdom 環境を拡張
### [jsdom](https://github.com/jsdom/jsdom) とは
前提として、ブラウザ上で動作する JavaScript は、生の JavaScript に加えてブラウザ操作用の API を利用可能な環境になっている。
この独自の API はブラウザに実装された機能なので、純粋な JavaScript には含まれていない。従ってブラウザ上では利用できるけど Node.js 上では利用できないオブジェクト等が多く存在する。

しかし、そのままではブラウザ向けに実装した各種コードを Jest 等の Node.js ランタイムでテストしたい時などに不便である。
そこで Node.js のランタイム上でブラウザ独自の API を利用できるように、同じ動きをシミュレートできるような実装を提供してくれるのが jsdom である。

jest からは `jest.config.js` 内で下記のように設定することで jsdom の実装を global に反映している。

```
testEnvironment: "jest-environment-jsdom"
```

ところが、この jest-environment-jsdom が global オブジェクトを差し替えているために、 Node.js v18 で導入された global fetch が差し替え後の global に反映されておらず、実際に fetch を利用しようとすると `ReferenceError: fetch is not defined` というエラーが発生することになる。

### jsdom 環境を拡張する
[jest のドキュメント](https://github.com/jestjs/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string)によれば、 testEnvironment は自作できるらしい。

> You can create your own module that will be used for setting up the test environment. The module must export a class with `setup`, `teardown` and `getVmContext` methods. You can also pass variables from this module to your test suites by assigning them to `this.global` object – this will make them available in your test suites as global variables. The constructor is passed `globalConfig` and `projectConfig` as its first argument, and `testEnvironmentContext` as its second.

これを利用して、[このコメント](https://github.com/jsdom/jsdom/issues/1724#issuecomment-1446858041)で提案されている方法を試してみる。

```ts:FixJSDOMEnvironment.ts
import JSDOMEnvironment from "jest-environment-jsdom";

// https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string
export default class FixJSDOMEnvironment extends JSDOMEnvironment {
  constructor(...args: ConstructorParameters<typeof JSDOMEnvironment>) {
    super(...args);

    // FIXME https://github.com/jsdom/jsdom/issues/1724
    if (!this.global.fetch) {
      this.global.fetch = fetch;
      this.global.Headers = Headers;
      this.global.Request = Request;
      this.global.Response = Response;
    }
  }
}
```

```ts:jest.config.ts
{
  "testEnvironment": "./FixJSDOMEnvironment.ts"
}
```

この状態で jest を実行することで、無事に node-fetch の polyfill なしで Node.js 標準の fetch API を利用することができた。

## MSW の Node.js v18 対応
無事に Node.js 標準の fetch API を利用することができたものの、別の問題として API のモックに利用している [MSW](https://github.com/mswjs/msw) が Node.js v18 に対応しておらず、 API アクセスをモックできないことが分かった。

公式の issue を見ると、2023年1月時点では対応の意思こそあるもののメンテナが忙しくて作業できていないという悲痛なコメントが見られる。
https://github.com/mswjs/msw/issues/1388#issuecomment-1401706972

## 結論
以上より、まだ Experimental な Node.js 純正の fetch API にこだわるよりも、 `jsdom` や MSW の対応が完了するまでは今まで通り `node-fetch` による polyfill に頼った方が良さそうに思った。
（MSW を使っていないプロジェクトなら問題ないかもしれないが、そこは各々で判断してほしい。）

```js:jest.setup.js
// 今まで通りの polyfill
import "isomorphic-fetch"
```

## 参考
- https://github.com/jestjs/jest/issues/13834#issuecomment-1407375787
- https://github.com/jsdom/jsdom/issues/1724#issuecomment-1446858041
- [jest-environment-jsdom](https://github.com/jestjs/jest/blob/v29.5.0/packages/jest-environment-jsdom/src/index.ts)