グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



プロダクトにReact Testing Library(RTL)を導入してみてハマったこと

みなさんテスト書いてますか。

恥ずかしながら、筆者が開発中のプロダクトでは、現状ではお世辞にもテストがあるとは言えない状況です。

今回は開発中プロダクトにReact Testing Library(RTL)を導入してハマったことについてご紹介したいと思います。

React Testing Library(RTL)の導入

テストランナーにはjestを使用するのでjestをまずインストールしていきます。

npm install --save-dev \
  jest \
  ts-jest \
  jest-styled-components \
  @types/jest

続いて React Testing Library(RTL) をインストールします。

npm install --save-dev \
  @testing-library/react \
  @testing-library/jest-dom \
  @testing-library/react-hooks \
  @testing-library/user-event

jestの設定ファイル jest.config.ts を追加します。

jest.config.ts
module.exports = {
  roots: ["<rootDir>/src"],
  testMatch: [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)",
  ],
  transform: {
    "^.+\\.(ts|tsx)$": "ts-jest",
  },
};
 package.json にテスト用のnpm scriptsを追加します。

package.json
   "scripts": {
     "dev": "vite",
+    "test": "jest",
+    "test:watch": "jest --watch",
     "build:dev": "tsc && vite build",
     "serve": "vite preview"
   },

これで npm run test テストを実行できるようになります。

Vite編

Viteで設定したaliasがjestで読み込まれない

Error
Cannot find module '@/path/to/module' from 'src/components/xxx.tsx'

原因

Viteで  import { Component } from @/components/…  のようにimport出来るようにaliasを設定している。
Viteでaliasを設定していて、  @/path/to/module  のように絶対パスでimport構文を定義していました。 jestはVite経由で実行されないので、jestがaliasを解釈できずにエラーとなっているようです。

※例
vite.config.ts
import { resolve } from "path";

import reactRefresh from "@vitejs/plugin-react-refresh";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRefresh()],
  resolve: {
    alias: {
      "@": resolve(__dirname, "src"),
    },
  },
});

対策

aliasの設定をjestでパスを解決する仕組み moduleNameMapper で設定します。

jest.config.js
    transform: {
      "^.+\\.(ts|tsx)$": "ts-jest",
    },
 +  moduleNameMapper: {
 +    "^@/(.*)$": "<rootDir>/src/$1",
 +  },
  };

import.meta 使用箇所でエラー

Error
error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node12', or 'nodenext'.

原因

 import.meta  を使用している。
 import.meta はECMA Script Module (ESM) 、もっというと  es2020  以降でしか動作しないようで、 ts-jest で対象ファイルをCommonJS形式にコンパイルしてしまうため、 import.meta を解釈できずにエラーになっていると思われます。

対策

調べた限り解決方法は2パターン

  1. テスト実行環境をESM対応する
  2.  import.meta  process.env  に書き換える
 1.  を試した結果、styled-componentsがESM対応出来ておらず、 styled.span is a not function となってしまいstyled-components使用コンポーネントでエラーになり、他にもESM対応で悉くエラーになりつらみ。

また、 1.  の対応をしても  import.meta  のところは結局改善せず、 2. の対応またはmock対応を行わないといけないので一旦ESM対応は断念しました。

そのため、 2. の方法で、 import.meta  使用箇所を  process.env  に置き換えることで対応しました。

vite.config.ts
import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd())

  // expose .env as process.env instead of import.meta since jest does not import meta yet
  const envWithProcessPrefix = Object.entries(env).reduce(
    (prev, [key, val]) => {
      return {
        ...prev,
        ['process.env.' + key]: `"${val}"`,
      }
    },
    {},
  )

  return {
    define: envWithProcessPrefix,
  }
})

RTLがReactコンポーネントを解釈できない

Error
TypeError: Cannot read properties of undefined (reading 'createElement')

       7 |
       8 | test("SomeComponent", () => {
    >  9 |   render(<SomeCOmponent />);
         |          ^
      10 |   screen.debug();
      11 |   expect(screen.getByText("Hello Test")).toBeInTheDocument();
      12 | });

原因

tsconfig.json の  esModuleInterop  false になっている。

対策

tsconfig.json
-    "esModuleInterop": false,
+    "esModuleInterop": true,

esModuleInteropとは何か

default exportを使用しているライブラリのCommonJS形式へのコードコンパイル時に、importができないため相互運用性を高めるためのオプション。 今回のケースだと、ts-nodeがCommmonJS形式にコンパイルされたdefault exportのReactをimportできないため発生したエラーと思われます。

TODO: Viteのdevサーバだとなんで動くのか

ここは理解不足なのですがjestはCommonJS形式、ViteはESM形式で動作してるから?

Jest編

jestでtest environment関連のエラー

Error
The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string.
Consider using the "jsdom" test environment.

原因

jest v27から  testEnvironment  のデフォルト値が  jsdom  から  node  に変更になった。

対策

jestの設定で  testEnvironment  の値を  jsdom  に設定します。

jest.config.js
   moduleNameMapper: {
     "^@/(.*)$": "<rootDir>/src/$1",
   },
+  testEnvironment: "jsdom",
 };

RTL編

RTLで toBeInTheDocument is not a function

Error
TypeError: expect(...).toBeInTheDocument is not a function

原因

テストファイルに  import ‘@testing-library/jest-dom’  の記述がない。またはjset-domの設定がされていない。

Usage

Import @testing-library/jest-dom once (for instance in your tests setup file) and you’re good to go:

// In your own jest-setup.js (or any other name) import ‘@testing-library/jest-dom’

// In jest.config.js add (if you haven’t already) setupFilesAfterEnv: [‘/jest-setup.js’]https://github.com/testing-library/jest-dom#usage

対策

一括で設定する場合

jest.config.js
     "^@/(.*)$": "<rootDir>/src/$1",
   },
   "testEnvironment": "jsdom",
+  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
 };
jest.setup.ts
import "@testing-library/jest-dom";

RTLの screen.debug() の出力表示が省略される

原因

デフォルトでは7000文字で丸め込まれる。

対策

環境変数に  DEBUG_PRINT_LIMIT  を追加します。

DEBUG_PRINT_LIMIT=10000

return component(render) hooksパターンのテストが通らない

汎用的なコンポーネントを再利用するためにreturn component(render) hooksパターンをモーダルなどで活用しています。
例としてはこのようなカスタムフックです。

useModal.tsx
import React, { useCallback, useState } from "react";

import { Modal } from "./Modal";

type RenderProps = {
  children: JSX.Element;
};

export const useModal = (): readonly [
  ({ children }: RenderProps) => JSX.Element,
  () => void
] => {
  const [isOpen, setIsOpen] = useState(false);
  const open = useCallback(() => setIsOpen(true), []);
  const close = useCallback(() => setIsOpen(false), []);

  const Component = useCallback(
    ({ children }: RenderProps) => {
      return (
        <Modal open={isOpen} onClose={close}>
          {children}
        </Modal>
      );
    },
    [close, isOpen]
  );

  return [Component, open] as const;
};

このカスタムフックのテストを書いて実行すると、なぜかテストが通らずえらくハマりました。

useModal.test.tsx
import { render, screen } from "@testing-library/react";
import { renderHook } from "@testing-library/react-hooks";
import userEvent from "@testing-library/user-event";
import React from "react";

import { useModal } from "./useModal";

test("モーダルを開くまではモーダルは表示されない", async () => {
  const { result } = renderHook(() => useModal());
  const [Modal] = result.current;

  render(
    <>
      <button onClick={open}></button>
      <Modal>
        <span>コンテンツ</span>
      </Modal>
    </>
  );

  expect(screen.queryByText("コンテンツ")).not.toBeInTheDocument();
});

test("モーダルを開くとモーダルが表示される", async () => {
  const { result } = renderHook(() => useModal());
  const [Modal, open] = result.current;

  render(
    <>
      <button onClick={open}></button>
      <Modal>
        <span>コンテンツ</span>
      </Modal>
    </>
  );
  const button = screen.getByRole("button");
  userEvent.type(button, "{enter}");

  expect(screen.getByText("コンテンツ")).toBeInTheDocument();
});
Error
● モーダルを開くとモーダルが表示される

    TestingLibraryElementError: Unable to find an element with the text: コンテンツ. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.

    Ignored nodes: comments, <script />, <style />
    <body>
      <div>
        <button />
      </div>
    </body>

      34 |   userEvent.type(button, "{enter}");
      35 |
    > 36 |   expect(screen.getByText("コンテンツ")).toBeInTheDocument();
         |                 ^
      37 | });
      38 |

原因

原因については詳細は不明ですが、RTLの  renderHooks  を使って今回のようなコンポーネントを返すhooksをテストするとReactのReconciliationが実行されないようです。

対策

このようなパターンの時は  renderHooks  ではなくReactコンポーネントをそのまま使う。

useModal.test.ts
test("モーダルを開くとモーダルが表示される", async () => {
  const Component = () => {
    const [Modal, open] = useModal();

    return (
      <>
        <button onClick={open}></button>
        <Modal>
          <span>コンテンツ</span>
        </Modal>
      </>
    );
  };

  render(<Component />);
  const button = screen.getByRole("button");
  userEvent.type(button, "{enter}");

  expect(screen.getByText("コンテンツ")).toBeInTheDocument();
});

こうすることでテストが通るようになりました。

番外編

ts-jest から @swc/jest に変更する

 @swc/jest  をインストールします。

npm i -D jest @swc/core @swc/jest

jestの設定に  @swc/jest  の設定を追加します。

jest.config.js
   transform: {
-    "^.+\\.(ts|tsx)$": "ts-jest",
+    "^.+\\.(ts|tsx)$": [
+      "@swc/jest",
+      {
+        sourceMaps: "inline",
+        module: {
+          type: "commonjs",
+        },
+        jsc: {
+          parser: {
+            syntax: "typescript",
+            tsx: true,
+          },
+          transform: {
+            react: {
+              runtime: "automatic",
+            },
+          },
+        },
+      }
+    ],
   },

速度比較

Before After
Test Suites: 3 passed, 3 total 3 passed, 3 total
Tests: 75 passed, 75 total 75 passed, 75 total
Snapshots: 0 total 0 total
Time: 2.818 s, estimated 3 s 0.966 s, estimated 1 s

おおよそ3倍くらい早くなりました。 体感としても早くなり、とても気持ちがいいです。

さいごに

簡易的ですが、React Testing Library(RTL)を導入してみてハマったことについてお伝えしました。
アサーションやテストのやり方について覚えることが多く中々テストを書けていませんでしたが、徐々にでもテストを拡充していきたいですね。

この記事を書いた人

美髭公
美髭公事業開発部 web application engineer
2013年にアーティスに入社。システムエンジニアとしてアーティスCMSを使用したWebサイトや受託システムの構築・保守に携わる。環境構築が好き。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします