Next.js x TypeScriptのブログにJestを導入した。

概要

仕事で Next.js を使うことになったので、Jest に入門しました。色々調べてながら当ブログ(Next.js x TypeScript x Vercel の構成)にも Jest を導入しました。

その時の PR がこちらです。
スナップショットテストを追加 by kenzo-tanaka · Pull Request #179 · kenzo-tanaka/nextJsBlog

Jest を導入する

セットアップ手順としては、ざっくり以下の 2 つです。

  • パッケージのインストール
  • 設定ファイルの作成
yarn add 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 --dev

jest.config.jsの設定。zenn-editor のjest.config.jsも参考にしつつ、自分の環境で必要そうな設定に絞って入れました。

jest.config.js
module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
  transform: {
    "^.+\\.(ts|tsx)$": "ts-jest",
  },
  moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
  // https://github.com/zeit/next.js/issues/8663#issue-490553899
  globals: {
    "ts-jest": {
      tsconfig: "<rootDir>/tsconfig.jest.json",
    },
  },
};

tsconfig.jest.jsonの設定。これは TypeScript のコードをコンパイルする時の設定値。

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

スナップショットテストを書く

スナップショットテストはプログラムの出力を前回の出力と比較して、差分がないかをチェックするテストです。意図しない UI の変更が生じていないかをチェックできます。

スナップショットテスト · Jest

スナップショットの置き場

スナップショットのテストの置き場ですが、Next.js の example 含め色々リポジトリを見てみたのですが、人によって置き場が分かれている感じでした。

  • ./__tests__: 公式の example はこちら
  • ./componennts/__tests__: 人によってはこちら

どちらに置いてもテストは実行できるため、自分のブログの場合は、最終的に /components/__tests__下に置きました。こちらのほうがコンポーネントファイルとの距離が近いので、可視性が高いかなと考えたからです。

公式 example の書き方

公式の example のコードでは、以下のようにスナップショットテストが書かれていました。

snapshot.js
import React from 'react'
import renderer from 'react-test-renderer'
import Index from '../pages/index'

it('renders homepage unchanged', () => {
  const tree = renderer.create(<Index />).toJSON()
  expect(tree).toMatchSnapshot()
})

next.js/snapshot.js at canary · vercel/next.js

これでも問題なくテストできるのですが、以下の Warning が出力されます。

  ●  Cannot log after tests are done. Did you forget to wait for something async in your test?
    Attempted to log "Warning: An update to Link inside a test was not wrapped in act(...).

    When testing, code that causes React state updates should be wrapped into act(...):

    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */

    This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
        at Link (/Users/tanakakenzou/Documents/personal/nextjs-new-blog/node_modules/next/client/link.tsx:137:19)
        at div
        at MobileCategoryMenu".

これの解消は色々模索したのですが、最終的には createRenderer関数を使うと上記は出ないことが分かったのでそちらに変更。

CategoryMenu.test.tsx
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import CategoryMenu from "../categoryMenu";

jest.mock("next/router", () => ({
  useRouter() {
    return {
      route: "/",
      pathname: "",
      query: "",
      asPath: "",
    };
  },
}));

const renderer = createRenderer();
test("CategoryMenu", () => {
  renderer.render(<CategoryMenu />);
  const renderedOutput = renderer.getRenderOutput();
  expect(renderedOutput).toMatchSnapshot();
});

この修正の差分はこちらです。 test: snapshot を update してテストを実行 & profile.test.tsx の実装で act の warning が出ないよう修正 · kenzo-tanaka/nextJsBlog@ba32cb8

useRouterのモックを作成

僕のブログのカテゴリーメニューとかは現在のパスを見るために、useRouterを使っています。useRouterを使っているコンポーネントをスナップショットテストしようとすると、TypeError: Cannot read property ... of nullとなり実行できませんでした。

Next.js のディスカッションを見ると、ここはモックを作成する必要がありそうなので、その対応も行いました。
How to mock useRouter? · Discussion #23034 · vercel/next.js

CategoryMenu.test.tsx
jest.mock("next/router", () => ({
  useRouter() {
    return {
      route: "/",
      pathname: "",
      query: "",
      asPath: "",
    };
  },
}));

参考記事、リポジトリ