NextJSでreact-testing-library、Jestを利用する
2023-02-09
a year ago
開発環境
- next 13.1.2
- react 18.2.0
- jest 29.3.1
- msw 1.0.0
- next-page-tester 0.33.0
前提
環境のセットアップとLink、SSGのテストコードの実装例の紹介になります。
APIはJsonPlaceholderを利用しています。
本題
環境構築
まずJestが組み込まれたテンプレートを利用してNextのプロジェクトを立ち上げ、モックサーバを利用したいので、mswをインストールします。
$ yarn create next-app --example with-jest [プロジェクト名]
$ yarn add msw
次に設定ファイルを追加編集していきます。
$ touch next.config.js
module.exports = {
trailingSlash: true,
};
{
"name": "test-nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest --env=jsdom --verbose -u", // 変更
"test:ci": "jest --ci"
},
...省略
}
const nextJest = require('next/jest');
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
});
// Add any custom config to be passed to Jest
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
// Handle module aliases (this will be automatically configured for you soon)
// '^@/components/(.*)$': '<rootDir>/components/$1',
// '^@/pages/(.*)$': '<rootDir>/pages/$1',
'^@/(.*)$': '<rootDir>/src/$1',
},
testEnvironment: 'jest-environment-jsdom',
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig);
// Optional: configure or set up a testing framework before each test.
// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`
// Used for __tests__/testing-library.js
// Learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
import { TextEncoder, TextDecoder } from 'util';
// Polyfill for encoding which isn't present globally in jsdom
if (typeof global.TextEncoder === 'undefined') {
global.TextEncoder = TextEncoder;
}
if (typeof global.TextDecoder === 'undefined') {
global.TextDecoder = TextDecoder;
}
Linkのテスト
まず必要なライブラリをインストールします。
$ yarn add -D next-page-tester
※indexページにnext/linkを利用したナビゲーションバーがあることを前提にしています。
※各種Linkにはdata-testidのような一意の属性を持たせることを前提にしています。
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getPage, initTestHelpers } from 'next-page-tester';
initTestHelpers();
describe('Navigation by Link', () => {
it('Should selected link page navigations', async () => {
// 1. ページを取得
const { page } = await getPage({
route: '/index',
});
// 2. ページをレンダリング
render(page);
// 3. ユーザーイベント発生(data-testid = 'a-nav'のLinkコンポーネントをクリック)
await waitFor(() => userEvent.click(screen.getByTestId('a-nav')));
// 4. 期待する動作(上記リンク先のページに「A Page」という文字がある)
expect(await screen.findByText('A Page')).toBeInTheDocument();
await waitFor(() => userEvent.click(screen.getByTestId('b-nav')));
expect(await screen.findByText('B Page')).toBeInTheDocument();
screen.debug(); // 必要に応じて
});
});
next-page-testerを利用する際は非同期で実行することになります。
ユーザーイベントの発生をwaitForで実行しないとテストはパスしますが、ログに警告が出るのでこちらの書き方が推奨されているようです。
SSGのテスト
SSGで実装された一覧画面、詳細画面のテスト例を実装してみます。
一覧画面(※ブログ記事を想定)
import { render, screen, cleanup } from '@testing-library/react';
import { getPage } from 'next-page-tester';
import { initTestHelpers } from 'next-page-tester';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
initTestHelpers();
// 1. apiを叩いた時のレスポンスデータを定義
const handlers = [
rest.get('https://jsonplaceholder.typicode.com/posts/', (req, res, ctx) => {
const _limit = req.url.searchParams.get('_limit');
if (_limit === '10') {
return res(
ctx.status(200),
ctx.json([
{
id: 1,
title: 'dummy title 1',
body: 'dummy body 1',
userId: 1,
},
{
id: 2,
title: 'dummy title 2',
body: 'dummy body 2',
userId: 2,
},
])
);
}
}),
];
// 2. モックサーバー連携
// 2-1. 設置
const server = setupServer(...handlers);
// 2-2. 起動
beforeAll(() => server.listen());
// 2-3. テストケースごとに副作用が起こらないように都度初期化
afterEach(() => {
server.resetHandlers();
cleanup();
});
// 2-4. 切断
afterAll(() => server.close());
describe('Blog Page', () => {
it('Should render the list of blogs pre-fetched by SSG', async () => {
const { page } = await getPage({
route: '/blog-page',
});
render(page); // 構造の取得
expect(await screen.findByText('Blog Page')).toBeInTheDocument();
expect(screen.getByText('dummy title 1')).toBeInTheDocument();
expect(screen.getByText('dummy title 2')).toBeInTheDocument();
});
});
詳細画面
import { render, screen, cleanup } from '@testing-library/react';
import { getPage } from 'next-page-tester';
import { initTestHelpers } from 'next-page-tester';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import userEvent from '@testing-library/user-event';
initTestHelpers();
const handlers = [
rest.get('https://jsonplaceholder.typicode.com/posts/', (req, res, ctx) => {
const _limit = req.url.searchParams.get('_limit');
if (_limit === '10') {
return res(
ctx.status(200),
ctx.json([
{
id: 1,
title: 'dummy title 1',
body: 'dummy body 1',
userId: 1,
},
{
id: 2,
title: 'dummy title 2',
body: 'dummy body 2',
userId: 2,
},
])
);
}
}),
rest.get('https://jsonplaceholder.typicode.com/posts/1', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
id: 1,
title: 'dummy title 1',
body: 'dummy body 1',
userId: 1,
})
);
}),
];
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => {
server.resetHandlers();
cleanup();
});
afterAll(() => server.close());
describe('Blog detail page', () => {
it('Should render detail content of id=1', async () => {
const { page } = await getPage({
route: '/posts/1',
});
render(page);
// 初めにコンテンツの取得するまでawaitをかける
// findはPromiseを返す
expect(await screen.findByText('dummy title 1')).toBeInTheDocument();
expect(screen.getByText('dummy body 1')).toBeInTheDocument();
});
it('Should route back top-page from blog detail page', async () => {
const { page } = await getPage({
route: '/posts/1',
});
render(page);
// コンテンツを取得できるまで待つ
await screen.findByText('dummy title 1');
// クリックイベントを発火
userEvent.click(screen.getByTestId('back-blog'));
// イベント発火後なのでawaitかける
expect(await screen.findByText('Blog Page')).toBeInTheDocument();
});
});
さいごに
react-queryやuseSWRなどライブラリを絡めたテストも今後調べていきたいです。