Next.js:jest でテストする

react-markdownremark-gfm を使ったコンポーネントのテストコードを jest で実行する設定のメモです。要点を挙げるなら、

  • JSDOM で未実装のメソッドはモックを用意しておく
  • transformIgnorePatterns にトランスコンパイルさせるものを設定しておく

といった辺りです。

テンプレ作成

create-next-app@14.2.4 で以下を指定。

react-markdownremark-gfm を追加。

npm install \
  react-markdown \
  remark-gfm

npm install -D @tailwindcss/typography

jest 関連のパッケージを追加。

npm install -D \
  @testing-library/jest-dom \
  @testing-library/react \
  @types/jest \
  jest \
  jest-environment-jsdom

packages.jsonscriptstest を追加。

{
  "name": "my-app",
  "version": "0.1.0
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest"
  },
  "dependencies": {
    "next": "14.2.4",
    "react": "^18",
    "react-dom": "^18",
    "react-markdown": "^9.0.1",
    "remark-gfm": "^4.0.0"
  },
  "devDependencies": {
    "@tailwindcss/typography": "^0.5.13",
    "@testing-library/jest-dom": "^6.4.6",
    "@testing-library/react": "^16.0.0",
    "@types/jest": "^29.5.12",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.4",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

設定の追加

tsconfig.jsontypes@testing-library/jest-dom を追加。
しないとテストの関数の型などが解決されません。

{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,tsconfig.json
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    },
    "types": [
      "@testing-library/jest-dom"
    ],
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

jest の設定は JavaScript にしました。
TypeScript だとその設定も必要の様なので妥協しました。
packages.json と同じディレクトリに次のファイルを用意します。

・jest.setup.js
 公式にある通り、jest で使用される DOM で未実装のメソッドはモックにしておきます。

import "@testing-library/jest-dom";

// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(window, "matchMedia", {
  writable: true,
  value: jest.fn().mockImplementation((query) => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(), // deprecated
    removeListener: jest.fn(), // deprecated
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

・jest.config.js
 react-markdownremark-gfm を足した後は、node_modules 配下に依存パッケージも含め、エラーになるものが結構出ました。

個々に設定するのが辛かったので、node_modules 配下は全部トランスコンパイルするように transformIgnorePatterns に設定しました。不都合がでたら見直すつもり。

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"],
  testEnvironment: "jsdom",
  moduleNameMapper: {
    "@/(.*)$": "<rootDir>/$1",
  },
};

module.exports = async () => ({
  ...(await createJestConfig(customJestConfig)()),
  transformIgnorePatterns: [
    `node_modules/(?!.*)/`,
  ],
});

ページとテストコード

以前のページで試してみます。

import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";

const md = `
# GFM

## Autolink literals

www.example.com, https://example.com, and contact@example.com.

## Footnote

A note[^1]

[^1]: Big note.

## Strikethrough

~one~ or ~~two~~ tildes.

## Table

| a | b  |  c |  d  |
| - | :- | -: | :-: |

## Tasklist

* [ ] to do
* [x] done
`;

const Home = () => {
  return <ReactMarkdown remarkPlugins={[remarkGfm]} className="prose">{md}</ReactMarkdown>;
};

export default Home;

テストコードは以下で。

import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import Page from "./page";

describe("Page", () => {
  it("renders a heading", () => {
    render(<Page />);
    const headers = screen.getAllByRole("heading");
    expect(headers.length).toEqual(7);
  });
});

テストの実行

npm run test で実行します。

% npm run test

> my-app@0.1.0 test
> jest

(node:17255) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
 PASS  app/page.test.tsx
  Page
    ✓ renders a heading (51 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.002 s
Ran all test suites.

コメント