この記事の概要
Next.js + TypeScriptのプロジェクトにJestを導入する手順を解説した記事です。
対象読者
- Next.jsを触った事がある人
- TypeScriptの基礎的な知識がある人
- Jestについて基礎的な知識がある人
この記事を書こうと思った動機
最近Next.jsで個人開発を始めました。
Next.jsはReactベースのフレームワークです。
Reactの create-react-app に似たcreate next-appというコマンドがあります。(プロジェクトのテンプレートを自動生成するツールです)
ただしReactの 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
をインストールします。
npm install -D jest ts-jest @testing-library/react @testing-library/react-hooks @types/jest jest-fetch-mock
yarn add jest ts-jest @testing-library/react @testing-library/react-hooks @types/jest jest-fetch-mock --dev
必要な設定ファイルを作成する
以下の3つのファイルを作成します。
import fetchMock from 'jest-fetch-mock';
fetchMock.enableMocks();
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}
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
の設定は一見不要ですが、これがないと下記と同様の問題が発生しました。
こちら に対処法が書かれているので参考にさせて頂きました。
テストコード(スナップショットテスト)
テスト対象のComponentは下記の通りです。
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;
対応するスナップショットテストは下記の通りです。
/**
* @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の関数のテストも用意します。
// テスト対象の関数
export const square = (num: number) => {
return num * 2;
};
// 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からリポジトリの一覧を取得するカスタムフックです。
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;
以下のようにテストコードを実装します。
/**
* @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
に以下を定義します。
{
"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 スナップショットテスト
おわりに
今回作成したテストやComponentはかなり単純な物なので、開発が進むにつれ、設定等を追加する事になると思います。
その場合はまた何らかの形で情報発信を行います。
以上になります。最後まで読んで頂きありがとうございます。