Next.js使ったプロジェクトでUnitテストを書く

# はじめに

- この記事はNext.js使っていて、これからUnitテスト書こうと思っている方向けの記事です
- 大まかな流れ
    - 他のNext.jsプロジェクトでどんなテストライブラリ使っているか調べる
    - テスト環境を整える
    - 簡単なテストを書いてみる
    - How to
    - まとめ

# Next.js + Typescript構成でよく使われているテストライブラリ

- jestはどこのプロジェクトでも使っていそう
- Reactのコンポーネントのテストは Enzyme vs React Testing Library という感じ
    - EnzymeとReact Testing Libraryの比較は以下の記事がとても参考になり読ませていただきました
        - [Enzyme vs React Testing Library結局どっちがいいのか問題に対する個人的な回答 - Qiita](https://qiita.com/nnishimura/items/521af77125d2b7ad49c5)
    - 今回はUnitテストが書きやすそうな `Enzyme` でやっていこうと思います

# 環境構築や準備

各バージョンや環境

```
- nextjs: 10.0.1
- enzyme: 3.11.0
- enzyme-adapter-react-16: 1.15.5
- jest: 26.6.3
- react-test-renderer: 17.0.1
```

もろもろ必要なパッケージをインストール

```bash
$ yarn add -D jest ts-jest react-test-renderer enzyme enzyme-adapter-react-16 enzyme-to-json @types/react-test-renderer @types/jest @types/enzyme-adapter-react-16
```

### [react-test-renderer](https://www.npmjs.com/package/react-test-renderer)とは?

- React コンポーネントをピュアな JavaScript オブジェクトにレンダーすることができる React レンダラを提供
- スナップショットテストで使用
- 出力を走査して特定のノードを検索し、それらに対してアサーションを行うことも可能

## 1. 簡単なテストを動かせるまでの環境構築

### 1-1. テスト用のディレクトリ作成

プロジェクトの直下に `__tests__` ディレクトリを作成し、テストや設定はこちらに実装していきたいと思います。
この中に `setupTests.ts` と `tsconfig.jest.json` を以下内容で作成します。

```tsx:setupTests.ts
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

process.env = {
	...process.env,
	__NEXT_IMAGE_OPTS: {
		deviceSizes: [320, 420, 768, 1024, 1200],
		imageSizes: [],
		domains: ['images.example.com'],
		path: '/_next/image',
		loader: 'default',
	} as any,
};
```

`__NEXT_IMAGE_OPTS` に関しては後述のバッドノウハウでも出てきますが、`next/image` を使っている箇所をテストする際に必要です。

```json:tsconfig.jest.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "jsx": "react"
  }
}
```

次にテスト対象のComponentで `css` ファイルやリソースファイルをimportしていた時にエラーになってしまうので、
モック用のファイルを作成してやります。(これもバッドノウハウで後述)

`__tests__/mocks/fileMock.js` と `__tests__/mocks/styleMock.js` を以下内容で作成します。

```jsx:fileMock.js
module.exports = 'test-file-stub';
```

```jsx:styleMock.js
module.exports = {};
```

両方ともモックなので読み込まれても特に何もしません。

### 1-2. `jest.config.js` の作成

上記を踏まえた `jest.config.js` を以下内容で作成します。

```jsx
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: [
    '<rootDir>/__tests__'
  ],
  setupFilesAfterEnv: ['<rootDir>/__tests__/setupTests.ts'],
  testPathIgnorePatterns: [
    '<rootDir>/__tests__/setupTests.ts',
    '<rootDir>/__tests__/tsconfig.jest.json',
    '<rootDir>/__tests__/mocks/'
  ],
  snapshotSerializers: ['enzyme-to-json/serializer'],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest'
  },
  moduleFileExtensions: [
    'ts',
    'tsx',
    'js',
    'jsx',
    'json',
    'node'
  ],
  // https://github.com/zeit/next.js/issues/8663#issue-490553899
  globals: {
    // we must specify a custom tsconfig for tests because we need the typescript transform
    // to transform jsx into js rather than leaving it jsx such as the next build requires. you
    // can see this setting in tsconfig.jest.json -> "jsx": "react"
    'ts-jest': {
      'tsconfig': '<rootDir>/__tests__/tsconfig.jest.json'
    }
  },
  collectCoverage: false,
  collectCoverageFrom: ["src/**/*"],
  coverageDirectory: "./coverage/",
  moduleNameMapper:{
    "\\.(css|less|sass|scss)$": "<rootDir>/__tests__/mocks/styleMock.js",
    "\\.(gif|ttf|eot|svg)$": "<rootDir>/__tests__/mocks/fileMock.js"
   }
};
```

# 簡単なテストを書いてみる

## 1. テスト対象のコンポーネント

テスト対象として以下の `Avator` コンポーネントでテストを書いてみます。
`Avator` コンポーネント自体はとてもシンプルで指定された画像を指定されたサイズの円内に表示するコンポーネントです。

```tsx
import React from 'react';

export type AvatorProps = {
  imageUrl: string;
  width: number;
  height: number;
};

const Avator: React.FC<AvatorProps> = ({ imageUrl, width, height }) => {
  return (
    <div>
      <img
        src={imageUrl}
        style={{
          width: width,
          height: height,
          borderRadius: '50%',
        }}
      />
    </div>
  );
};

export default Avator;
```

## 2. テストコードを書いてみる

`Avator` コンポーネントをテストするコードを書いてみたいと思います。
テスト内容としては `Avator` コンポーネント内に `<img>` 要素があって、そこに指定した画像URLとサイズがちゃんと設定されているかというものです。

```tsx
import { shallow } from 'enzyme';
import * as React from 'react';
import Avator from 'xxxx/Avator';

describe('Avator', () => {
  it('必要な要素に指定された値が設定されているはず', () => {
    const wrapper = shallow(
      <Avator
        imageUrl={'https://randomuser.me/api/portraits/women/26.jpg'}
        width={50}
        height={50}
      />
    );
    expect(wrapper.find('img')).toHaveLength(1);

    const src = wrapper.find('img').prop('src');
    expect(src).toEqual('https://randomuser.me/api/portraits/women/26.jpg');

    const style = wrapper.find('img').prop('style');
    expect(style.width).toEqual(50);
    expect(style.height).toEqual(50);
  });
});
```

Enzymeの[shallow render API](https://enzymejs.github.io/enzyme/docs/api/shallow.html)を使って `Avator` コンポーネント をshallow renderしたものを `wrapper` として受け取っています。
テストはこの `wrapper` を使って要素が存在するか、要素に指定した値が設定されているかをテストしています。
パッと見そこまで難しくはなく書きやすい感じですね ✨

# Enzyme How to
筆者がテスト書いていて「これどうやってテストするの？」と思った際のメモになります。

## 1. shallowで<img>の `src` 属性を取得したい時

```tsx
const wrapper = shallow(
  <App  />
);
const src = wrapper.find('img').prop('src');
```

[javascript - How do I get an attribute of an element nested in a React component using Jest and/or Enzyme? - Stack Overflow](https://stackoverflow.com/questions/41070895/how-do-i-get-an-attribute-of-an-element-nested-in-a-react-component-using-jest-a)

`find` と `props` を使う。

## 2. window.confirm を含めてテストしたい場合

```tsx
declare const global;

describe('...', () => {
  beforeEach(() => {
    global.window = {};
  });

  describe('confirm', () => {
    it('XXのメッセージが表示されるはず', () => {
      const mockCallWindow = jest.fn(() => true);
      global.window.confirm = mockCallWindow;

      const wrapper = shallow(
        <ConfirmDialog/>
      );
      wrapper.find('.show-button').simulate('click');
      expect(mockCallWindow).toHaveBeenCalledWith("XX");
    });
  });
```

[javascript - Simulate clicking "Ok" or "Cancel" in a confirmation window using enzyme - Stack Overflow](https://stackoverflow.com/questions/48728167/simulate-clicking-ok-or-cancel-in-a-confirmation-window-using-enzyme/48728455)[javascript - Simulate a button click in Jest - Stack Overflow](https://stackoverflow.com/questions/43747397/simulate-a-button-click-in-jest)

筆者の環境では上記でやったら上手く行きました。

## 3. useStateやuseEffectを使っているコンポーネントのテスト

[React Hooks Jest + enzyme + act で useEffect を含むコンポーネントのテストする - かもメモ](https://chaika.hatenablog.com/entry/2019/09/12/083000)

# バッドノウハウ

## 1.`TypeError: Cannot destructure property`deviceSizes`of 'undefined' or 'null'.` が発生する!

### エラーの詳細

```tsx
    TypeError: Cannot destructure property `deviceSizes` of 'undefined' or 'null'.

      at Object.<anonymous> (node_modules/next/client/image.tsx:62:9)
      at Object.<anonymous> (node_modules/next/image.js:1:107)
```

### 原因

`next/image` を使用している箇所で `deviceSizes` が設定されていない

### 解決方法

`setupTests.ts` に以下を追加する

```tsx
process.env = {
	...process.env,
	__NEXT_IMAGE_OPTS: {
		deviceSizes: [320, 420, 768, 1024, 1200],
		imageSizes: [],
		domains: ['images.example.com'],
		path: '/_next/image',
		loader: 'default',
	} as any,
};
```

### 参考リンク

- [Jest next/image: Cannot destructure property `deviceSizes` of 'undefined' or 'null' · Discussion #18373 · vercel/next.js](https://github.com/vercel/next.js/discussions/18373#discussioncomment-114212)
- [https://github.com/search?l=TypeScript&q=__NEXT_IMAGE_OPTS&type=Code](https://github.com/search?l=TypeScript&q=__NEXT_IMAGE_OPTS&type=Code)

## 2. テストで `sass` 読み込みでエラーになる

[Setting Up a Next.js Project With TypeScript, Sass, and Jest | by Aristos Markogiannakis | Better Programming | Medium](https://medium.com/better-programming/setting-up-a-next-js-project-with-typescript-sass-and-jest-d9b2d3bfb34a)
上記リンクの通りmock用のscssファイルとリソースファイルを用意してテスト時はそれを読み込むようにした。

## 3. `window` をテストの中でも使えるようにする

```tsx
declare const global;

describe('', () => {
  beforeEach(() => {
    global.window = {};
  });
  ...
  global.window = {};
  global.window.confirm = jest.fn(() => true);
);
```

# まとめ

EnzymeとJestでUnitテストはとても書きやすく、すんなりテストする事ができた印象でした。
ただ、Next.jsの場合、固有のエラーがちらほら起きたので、そこだけは地道に突破していくしか無さそうかなという感じです。

# 参考リンク

- [Enzyme vs React Testing Library結局どっちがいいのか問題に対する個人的な回答 - Qiita](https://qiita.com/nnishimura/items/521af77125d2b7ad49c5)
- [Next.js + TypeScriptのプロジェクトにJESTを導入する - Qiita](https://qiita.com/keitakn/items/0a714997eb058f2f67e2)
- [Reactのテストライブラリを比較する - react-test-renderer, enzyme, react-testing-library - Qiita](https://qiita.com/Hitomi_Nagano/items/8673be3c8907c6697cb6)
- [Enzymeでテキスト変更をsimulateする - Qiita](https://qiita.com/Mt_blue81/items/9002a79530384d960c22)