💫

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

2021/02/15に公開

はじめに

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

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

環境構築や準備

各バージョンや環境

- 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

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

$ 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とは?

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

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

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

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

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 を使っている箇所をテストする際に必要です。

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

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

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

fileMock.js
module.exports = 'test-file-stub';
styleMock.js
module.exports = {};

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

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

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

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 コンポーネント自体はとてもシンプルで指定された画像を指定されたサイズの円内に表示するコンポーネントです。

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とサイズがちゃんと設定されているかというものです。

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を使って Avator コンポーネント をshallow renderしたものを wrapper として受け取っています。
テストはこの wrapper を使って要素が存在するか、要素に指定した値が設定されているかをテストしています。
パッと見そこまで難しくはなく書きやすい感じですね ✨

Enzyme How to

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

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

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

findprops を使う。

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

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 Overflowjavascript - Simulate a button click in Jest - Stack Overflow

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

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

React Hooks Jest + enzyme + act で useEffect を含むコンポーネントのテストする - かもメモ

バッドノウハウ

1.TypeError: Cannot destructure propertydeviceSizesof 'undefined' or 'null'. が発生する!

エラーの詳細

    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 に以下を追加する

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,
};

参考リンク

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

Setting Up a Next.js Project With TypeScript, Sass, and Jest | by Aristos Markogiannakis | Better Programming | Medium
上記リンクの通りmock用のscssファイルとリソースファイルを用意してテスト時はそれを読み込むようにした。

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

declare const global;

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

まとめ

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

参考リンク

Discussion