プロダクトにReact Testing Library(RTL)を導入してみてハマったこと
みなさんテスト書いてますか。
恥ずかしながら、筆者が開発中のプロダクトでは、現状ではお世辞にもテストがあるとは言えない状況です。
今回は開発中プロダクトにReact Testing Library(RTL)を導入してハマったことについてご紹介したいと思います。
INDEX
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
"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パターン
- テスト実行環境をESM対応する
- import.meta を process.env に書き換える
また、 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の設定がされていない。
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)を導入してみてハマったことについてお伝えしました。
アサーションやテストのやり方について覚えることが多く中々テストを書けていませんでしたが、徐々にでもテストを拡充していきたいですね。
この記事を書いた人
- 2013年にアーティスに入社。システムエンジニアとしてアーティスCMSを使用したWebサイトや受託システムの構築・保守に携わる。環境構築が好き。
関連記事
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー