Next.js + TypeScriptのプロジェクトにJestを導入する

# この記事の概要
Next.js + TypeScriptのプロジェクトにJestを導入する手順を解説した記事です。

# 対象読者
- Next.jsを触った事がある人
- TypeScriptの基礎的な知識がある人
- Jestについて基礎的な知識がある人

# この記事を書こうと思った動機
最近Next.jsで個人開発を始めました。

Next.jsはReactベースのフレームワークです。

Reactの [create-react-app](https://github.com/facebook/create-react-app) に似たcreate next-appというコマンドがあります。（プロジェクトのテンプレートを自動生成するツールです）

ただしReactの [create-react-app](https://github.com/facebook/create-react-app) と違ってテスト実行環境は用意されていません。

そこでJESTを導入する手順を残しておきます。

# 実行環境
- Node.js 14.15.3
- Next.js 11.1.2
- React 17.0.2

# 具体的な手順とゴール

ここから具体的な手順を記載します。

JestでReactのスナップショットテストを実行出来る環境を構築する事を目標とします。

## 必要なpackageをインストールする

必要なpackageは以下の通りです。

```
jest
ts-jest
@testing-library/react
@testing-library/react-hooks
@types/jest
jest-fetch-mock
```

`npm` または `yarn` をインストールします。

```bash:npmを使う場合
npm install -D jest ts-jest @testing-library/react @testing-library/react-hooks @types/jest jest-fetch-mock
```

```bash:yarnを使う場合
yarn add jest ts-jest @testing-library/react @testing-library/react-hooks @types/jest jest-fetch-mock --dev
```

## 必要な設定ファイルを作成する

以下の3つのファイルを作成します。

```ts:test/setupTests.ts
import fetchMock from 'jest-fetch-mock';

fetchMock.enableMocks();
```

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

```javascript:jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  setupFilesAfterEnv: ['<rootDir>/test/setupTests.ts'],
  testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
  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>/test/tsconfig.jest.json',
    },
  },
};
```

`test/tsconfig.jest.json` と `jest.config.js` の `globals` の設定は一見不要ですが、これがないと下記と同様の問題が発生しました。

https://github.com/zeit/next.js/issues/8663

[こちら](https://github.com/zeit/next.js/issues/8663#issue-490553899) に対処法が書かれているので参考にさせて頂きました。

# テストコード（スナップショットテスト）

テスト対象のComponentは下記の通りです。

```tsx:src/components/AppTitle.tsx
import React from 'react';
import styled from 'styled-components';

const Title: React.FC = styled.h1`
  color: red;
  font-size: 50px;
`;

const AppTitle: React.FC = () => {
  return <Title>message</Title>;
};

export default AppTitle;
```

対応するスナップショットテストは下記の通りです。

```tsx:src/components/__tests__/AppTitle.spec.tsx
/**
 * @jest-environment jsdom
 */
import React from 'react';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render } from '@testing-library/react';
import AppTitle from '../AppTitle';

test('AppTitle', () => {
  const { asFragment } = render(<AppTitle />);
  expect(asFragment()).toMatchSnapshot();
});
```

# テストコード（通常のテストコード）

Reactではない通常のTypeScriptの関数のテストも用意します。

```ts:src/domain/Sample.ts
// テスト対象の関数
export const square = (num: number) => {
  return num * 2;
};
```

```ts:src/domain/__tests__/Sample.spec.ts
// Sample.ts squareのテストコード
import { square } from '../Sample';

describe('Sample.ts Functions TestCases', () => {
  it('should return the squared value', () => {
    const result = square(2);
    const expected = 4;

    expect(result).toStrictEqual(expected);
  });
});
```

# テストコード（カスタムフックのテストコード）

以下はGitHubのAPIからリポジトリの一覧を取得するカスタムフックです。

```typescript
import { useEffect, useState } from 'react';
import { CardListItem } from '../components/CardList';
import { fetchPublicRepos } from '../api/fetch/github';
import { isSuccessResult } from '../domain/asyncResult';

export type UseFetchPublicReposReturnType = {
  items?: CardListItem[];
};

const useFetchPublicRepos = (): UseFetchPublicReposReturnType => {
  const [items, setItems] = useState<CardListItem[]>();

  useEffect(() => {
    const fetchRepo = async () => {
      const publicRepos = await fetchPublicRepos();
      if (isSuccessResult(publicRepos)) {
        const cardListItems = publicRepos.value.map((gitHubRepository) => ({
          id: gitHubRepository.id,
          title: gitHubRepository.name,
          url: gitHubRepository.htmlUrl,
          description: gitHubRepository.description,
        }));

        setItems(cardListItems);
      }
    };

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    fetchRepo();
  }, []);

  return { items };
};

export default useFetchPublicRepos;
```

以下のようにテストコードを実装します。

```typescript
/**
 * @jest-environment jsdom
 */
import fetch from 'jest-fetch-mock';
import { renderHook, act } from '@testing-library/react-hooks';
import useFetchPublicRepos from '../useFetchPublicRepos';

describe('useFetchPublicRepos TestCases', () => {
  beforeEach(() => {
    fetch.resetMocks();
  });

  it('should be able to fetch public GitHub Repositories', async () => {
    const mockBody = [
      {
        id: 1,
        name: 'my-terraform',
        html_url: 'https://github.com/keitakn/my-terraform',
        description: '個人の検証用で使うTerraform',
      },
    ];

    const mockParams = {
      status: 200,
      statusText: 'OK',
    };

    fetch.mockResponseOnce(JSON.stringify(mockBody), mockParams);

    const expected = {
      items: [
        {
          id: 1,
          title: 'my-terraform',
          url: 'https://github.com/keitakn/my-terraform',
          description: '個人の検証用で使うTerraform',
        },
      ],
    };

    await act(async () => {
      const renderHookResult = renderHook(() => useFetchPublicRepos());

      await renderHookResult.waitFor(() => {
        expect(renderHookResult.result.current).toStrictEqual(expected);
      });
    });
  });
});
```

# テストの実行

`package.json` の `scripts` に以下を定義します。

```json:package.json
{
  "scripts": {
    "test": "jest",
    "test:coverage": "jest --collect-coverage"
  }
}
```

`yarn run test` もしくは `npm run test` を実行します。

以下のように表示されればテストが成功しています。

```
$ jest
 PASS  src/domain/__tests__/Sample.spec.ts
 PASS  src/components/__tests__/AppTitle.spec.tsx

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   1 passed, 1 total
Time:        1.628s, estimated 3s
Ran all test suites.
✨  Done in 2.23s.
```

`src/components/__tests__/AppTitle.spec.tsx` はスナップショットテストです。

初回はスナップショットファイルが生成されますので、これをバージョン管理に含めておきます。
これによってUIの意図しない防ぎます。

（参考） [Testing React Apps スナップショットテスト](https://jestjs.io/docs/ja/tutorial-react#%E3%82%B9%E3%83%8A%E3%83%83%E3%83%97%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%E3%83%86%E3%82%B9%E3%83%88)

# おわりに
今回作成したテストやComponentはかなり単純な物なので、開発が進むにつれ、設定等を追加する事になると思います。

その場合はまた何らかの形で情報発信を行います。

以上になります。最後まで読んで頂きありがとうございます。