Next.js + TypeScript + Jestで単体テスト環境を構築

### 概要
- 「Next.js」+「TypeScript」でのフロントエンド開発環境に向けて、Jestを使った単体テスをの設定を記載します。
- 特に、DOMコンポーネントのテスト向けに、「React Testing Library」を使った設定を記載します。
- 同様の設定に関する有用な記事は数多く、二番煎じどころではないが、バージョンや使うライブラリの組み合わせによってハマるポイントも多そうだったので、実際に直面したエラーとその解決策も載せておきます。

---
### 前提
- 環境
    - next.js: v.12.1.X
    - typescript: v.4.7.X
    - jest: v.28.1.X

---
### テスト対象となるコードを用意する
以下のコードをサンプルとして用意してみた。

```typescript
// src/components/Sample.tsx

import emailRegex from 'email-regex';

export const EmailForm = () => {
    const form = useForm({
        initialValues: { email: '' },
        validate: { email: (value) => emailRegex({exact: true}).test(value)? null : 'failed.' }
    });
    return (
        <form onSubmit={form.onSubmit((values) => console.log(values)}>
            <TextInput {...form.getInputProps('email')}/>
            <Button type="submit">
                Push
            </Button>
        </form>
    )
}
```

また、テストコードは以下のようなものを用意

```typescript
// src/components/Sample.spec.tsx
import { render, screen } from "@testing-library/react";
import { EmailForm } from "./specs/Sample";

it('changes the class when hovered', () => {
	render(<EmailForm />);
    screen.debug();
});

```
レンダリングしたHTMLを確認するだけのテスト。実際には想定しているタグが含まれているか？などをチェックすることになると思います。

---

### 基本編

#### パッケージインストール
[こちらの記事](https://zenn.dev/garypippi/articles/c79cb002e001681a73cd)を参考に、必要なパッケージのインストールと、設定ファイルの用意をします。
同時に、「React Testing Library」のパッケージもインストールしておきます。

```shell
yarn add --dev jest @types/jest ts-jest @testing-library/react
```

#### config設定
Jestのコンフィグを設定します。
`jest.config.js`ファイルを作成し、以下の設定を記述します。

```js
// jest.config.js
module.exports = {
    preset: 'ts-jest',
    globals: {
        'ts-jest': {
            tsconfig: './tsconfig.test.json'
        }
    }
}
```

すでに存在している（はずの）`tsconfig.json`に代わって、テスト用の`tsconfig.test.json`を読み込みます。
`tsconfig.test.json`は以下の通り。

```js
// tsconfig.test.json
{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "jsx": "react-jsx"
    }
}
```

ここで大事なポイントは、デフォルトの`tsconfig.json`の`compilerOptions`パラメータを上書きすることです。具体的には、`"fsx": "preserve"`となっている部分を`"jsx": "react-jsx"`に置き換えます。

[ドキュメント](https://www.typescriptlang.org/docs/handbook/jsx.html)によると、
- `preserve`は、Babel等によるトランスパイル処理が行われることを前提に、出力の一部が、JSXフォーマットになる方式。
- `react-jsx`は、React v.17から導入された変換方式に従って、JSXフォーマットをJSで解釈可能なフォーマットに変換する方式。

:::note warn
この設定を入れないと、テスト実行環境で、JSXを解釈できずに、下記のようなエラーが出るはず。

```
SyntaxError: Unexpected token '<'
```
:::

ここで `yarn jest` が通れば設定はおしまい

---

### `SyntaxError: Unexpected token 'export'`が発生した場合
[こちらの記事](https://media-massage.net/blog/ts-jest%E3%81%8C%E5%A4%96%E9%83%A8%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%82%92import%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AE%E5%AF%BE%E5%87%A6%E6%96%B9/)によると、ts-jestはESModuleに対応していないとのこと。
そして、導入したライブラリの中にESModuleで提供されているパッケージがあると、そのままではJest上でimportできないようです。

そのため、このようなエラーが出たときは、`babel-jest`を使った変換を指示してあげる必要があります。

#### パッケージインストール
babelを使った変換に必要なパッケージをインストールします。

```shell
yarn add --dev babel-jest @babel/preset-env @babel/plugin-transform-modules-commonjs
```

#### config設定
babelのコンフィグを設定します。
`babel.config.js`ファイルを作成し、以下の設定を記述します。

```js
// babel.config.js
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        },
      }
    ]
  ],
  plugins: [
    '@babel/plugin-transform-modules-commonjs',
  ]
}
```

Jestのコンフィグを修正します。
`jest.config.js`を下記のように修正します。

```js
// jest.config.js
module.exports = {
    transform: {
        '\\.jsx?$': 'babel-jest',
        '\\.tsx?$': 'ts-jest'
    },
	transformIgnorePatterns: [
        '/node_modules/(?!email-regex)'
	],
    globals: {
        'ts-jest': {
            tsconfig: './tsconfig.test.json'
        }
    }
}
```

ここで大事なポイントは、`transformIgnorePatterns`として、エラーが発生しているESModuleライブラリを変換対象に加えている点です。
否定先読み表現 `(?!hoge)` を使って、該当する名前に合致するライブラリ ***以外*** を変換対象外としています。

今回は、[email-regex](https://www.npmjs.com/package/email-regex)が該当しているため、上記のような書き方をしています。ここは環境に合わせて変更してください。

:::note warn
この設定を入れないと、ESModuleを変換してくれないため、以下のエラーが発生し、正しくimportできないことがわかる。

```
SyntaxError: Unexpected token 'export'
```
:::

---
### `The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string. Consider using the "jsdom" test environment.`が発生した場合

テスト環境の指定の修正が必要になります。デフォルトのままだと`Node.js`となっていますが、これを`
jest-environment-jsdom` に変更します。

#### パッケージインストール
`jest-environment-jsdom`をインストールします。

```shell
yarn add --dev jest-environment-jsdom
```

### config設定
Jestのコンフィグを修正します。
`jest.config.js`を下記のように修正します。

```js
// jest.config.js
module.exports = {
    transform: {
        '\\.jsx?$': 'babel-jest',
        '\\.tsx?$': 'ts-jest'
    },
	transformIgnorePatterns: [
        '/node_modules/(?!email-regex)'
	],
    testEnvironment: 'jsdom',
    globals: {
        'ts-jest': {
            tsconfig: './tsconfig.test.json'
        }
    }
}
```

---
### babelにより、Next.jsのSWCが無効化されて困った場合
`babel.config.json`を利用した際、Next.jsのSWCが無効化され、トランスパイルが思った通りに動かないことがあります。

:::note warn
トランスパイルが走った際に、以下のように、TypeScriptが正しく解釈できなかったことによるエラーが出ます。

```shell
info  - Disabled SWC as replacement for Babel because of custom Babel configuration "babel.config.js" https://nextjs.org/docs/messages/swc-disabled
info  - Using external babel configuration from /workspace/babel.config.js
wait  - compiling...
error - ./src/component/index.tsx:15:33
Syntax error: Unexpected token, expected ","
...
```
:::

このような場合、
1. `babel.config.json`のファイル名変更
    1. このままだと、自動的に読み込まれてbabelを利用したトランスパイル処理が走ってしまう。そこで、名前を、`babel.config.my.json`などに変えてあげる
1. `jest.config.json`のtransformオプションを追加
    1. jest用のconfigの中で、`configFile`を追加
    1. `configFile` には、上記名前変更したbabel用configファイルを指定

```js
// jest.config.js
module.exports = {
  transform: {
    '\\.jsx?$': ['babel-jest', { 'configFile': './babel.config.my.js' }],
    '\\.tsx?$': 'ts-jest'
  },
  transformIgnorePatterns: [
    '/node_modules/(?!email-regex)'
  ],
  testEnvironment: 'jsdom',
  globals: {
    'ts-jest': {
      tsconfig: './tsconfig.test.json'
    }
  }
}

```

これで動く（はず）

---

### おしまい
あとは`yarn jest`を実行すればテストが動きました。

```shell
# yarn jest
yarn run v1.22.19
$ /workspace/node_modules/.bin/jest
 PASS  src/components/Sample.spec.tsx (37.822 s)
  ✓ changes the class when hovered (184 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        42.163 s
Ran all test suites.
Done in 48.51s.

```