次回: CRAからViteへの移行 - Vite移行編 - harryのブログ
前書き
- 個人開発のSPAでcreate-react-app(以下CRA)を使っていたが、少し前ついに最新版に更新できないライブラリが出てきてしまった。
- ので、重い腰を上げて移行作業を始めた。
- が、テスト移行だけでしんどい事になってきたので、忘れないうちに記事を書く。
- つらすぎたので既にVite移行完了後はVitestへ移行することを心に決めつつある。
以下の記事で言及されている通り、テストをCRA依存からjest単体へ移行することは、Vite移行とは独立して作業できます。 そのため、まずはこちらから始めます。
移行前環境
https://github.com/harry0000/last-origin-unit-viewer/blob/v1.2.20/package.json
- CRA: 5.0.1
- jest: 27.5.1
- React: 18.2.0
- TypeScript: 5.3.3
ビルド環境
Node.js: 20系
移行作業
手順
- 対象リポジトリを作業ディレクトリとは異なる適当な場所にcloneして
npm run eject
を実行- このdiffを参考に必要な設定を確認する
- 必要なpackageのインストール
- Babel & jestの設定
- 個別のエラー対応など
- ライブラリアップデート
必要なpackageのインストール
ejectで出たdiffとjest公式ドキュメントを参考に必要なpackageをインストールする。
参照するのは "Setup without Create React App" の所。jestはlatestを指定しないと27.x系が入ってしまう。
npm install --save-dev jest@latest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
これ以外に必要なpackageは以下の通り。
@babel/preset-typescript
- TypeScriptで書いているため。
jest-environment-jsdom
- jest 28から別にインストールが必要になった。
- https://jest-archive-august-2023.netlify.app/docs/28.x/upgrading-to-jest28/#jsdom
- eject後のプロジェクトはjest 27なので、
dependencies
やdevDependencies
に入ってない。
- これが無いとテスト実行時、以下のようなエラーが出る。
● Test suite failed to run TypeError: Cannot read properties of undefined (reading 'html') at new JSDOMEnvironment (node_modules/jest-environment-jsdom/build/index.js:72:44)
identity-obj-proxy
- CSSモジュールのモックのため。
- Using with webpack · Jest
Babel & jestの設定
以下を参考に設定ファイルを追加する。
- jest公式ドキュメント
- 現在のpackage.jsonの
jest
の設定- package.jsonから削除して
jest.config.js
に移動する。
- package.jsonから削除して
- eject後のpackage.jsonの
babel
とjest
の設定- それぞれ必要な設定を
babel.config.js
とjest.config.js
に移動する。
- それぞれ必要な設定を
Vitestへ移行する前提で設定ファイルはtsではなくjsで書いてます。
package.json
- "test": "react-scripts test", + "test": "jest",
- "jest": { - "transformIgnorePatterns": [ - "/node_modules/(?!@react-dnd|react-dnd|dnd-core)" - ] - },
babel.config.js
plugins
の設定は書いてないとテスト実行時にめっちゃwarningが出る。- ので結局書くことになる。
module.exports = { plugins: [ ['@babel/plugin-transform-class-properties', { 'loose': true }], ['@babel/plugin-transform-private-methods', { 'loose': true }], ['@babel/plugin-transform-private-property-in-object', { 'loose': true }] ], presets: [ 'react-app', '@babel/preset-env', ['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript' ] };
jest.config.js
- 可能な限り必要最小限な設定にしてあります。
module.exports = { roots: [ '<rootDir>/src' ], testMatch: [ '**/?*\\.test\\.(ts|tsx)' ], modulePaths: [], testEnvironment: 'jsdom', moduleNameMapper: { '\\.css$': 'identity-obj-proxy' }, transform: { '^.+\\.[jt]sx?$': 'babel-jest' }, transformIgnorePatterns: [ '/node_modules/(?!@react-dnd|react-dnd|dnd-core)' ], setupFilesAfterEnv: [ '<rootDir>/src/setupTests.ts' ] };
.github/workflows/ci.yml
CI
環境変数が不要になるはず。
test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - name: Test - run: CI=true npm test + run: npm test
個別のエラー対応
SVGファイルのエラー
Reactコンポーネントのテストで以下のエラーが出ましたが、SVGファイルimportの考慮を忘れていました。*1
Details: D:\_git\last-origin-unit-viewer\src\component\icon\TwitterSocialIcon.svg:1 ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){<?xml version="1.0" encoding="utf-8"?> ^ SyntaxError: Unexpected token '<' 8 | import { Copy } from '../icon/FluentIcons'; 9 | import SVGButton from '../common/SVGButton'; > 10 | import { ReactComponent as TwitterSocialIcon } from '../icon/TwitterSocialIcon.svg'; | ^
jest.config.jsの moduleNameMapper
にSVGファイルのmockを設定します。
jest.config.js
moduleNameMapper: {
'\\.css$': 'identity-obj-proxy',
+ '\\.svg$': '<rootDir>/src/__mock__/svgMock.tsx'
},
src/__mock__/svgMock.tsx
import React from 'react'; const SvgMock: React.FC = (props) => (<svg {...props} />); export default SvgMock;
ちなみに、ejectしたプロジェクトでは涙ぐましい努力によって解決されていました。
config/jest/fileTransform.js
'use strict'; const path = require('path'); const camelcase = require('camelcase'); // This is a custom Jest transformer turning file imports into filenames. // http://facebook.github.io/jest/docs/en/webpack.html module.exports = { process(src, filename) { const assetFilename = JSON.stringify(path.basename(filename)); if (filename.match(/\.svg$/)) { // Based on how SVGR generates a component name: // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 const pascalCaseFilename = camelcase(path.parse(filename).name, { pascalCase: true, }); const componentName = `Svg${pascalCaseFilename}`; return `const React = require('react'); module.exports = { __esModule: true, default: ${assetFilename}, ReactComponent: React.forwardRef(function ${componentName}(props, ref) { return { $$typeof: Symbol.for('react.element'), type: 'svg', ref: ref, key: null, props: Object.assign({}, props, { children: ${assetFilename} }) }; }), };`; } return `module.exports = ${assetFilename};`; }, };
nanoid のエラー
render系のテストだけ以下のエラーで落ちました。
FAIL src/App.test.tsx ● Test suite failed to run Jest encountered an unexpected token Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax. Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration. By default "node_modules" folder is ignored by transformers. Here's what you can do: • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it. • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config. • If you need a custom transformation specify a "transform" option in your config. • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option. You'll find more details and examples of these config options in the docs: https://jestjs.io/docs/configuration For information about custom transformations, see: https://jestjs.io/docs/code-transformation Details: D:\_git\last-origin-unit-viewer\node_modules\nanoid\index.browser.js:1 ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import { urlAlphabet } from './url-alphabet/index.js' ^^^^^^ SyntaxError: Cannot use import statement outside a module 1 | import React from 'react'; > 2 | import { nanoid } from 'nanoid'; | ^
nanoidをトランスパイル設定を追加しました。
jest.config.js
transformIgnorePatterns: [ - '/node_modules/(?!@react-dnd|react-dnd|dnd-core)' + '/node_modules/(?!@react-dnd|react-dnd|dnd-core|nanoid)' ],
自分はお行儀悪くCRAに入ってるnanoidをそのまま使ってるので、後の移行作業で最新版を使うように変える必要がありそうです。
% npm ls nanoid last-origin-unit-viewer@1.2.20 D:\_git\last-origin-unit-viewer `-- react-scripts@5.0.1 `-- postcss@8.4.31 `-- nanoid@3.3.7
ライブラリアップデート
最後に、@testing-library/jest-dom
が 5.17.0 と古くなっているため、現最新の 6.2.0 にアップデートしておきます。
npm i -D @testing-library/jest-dom@latest
色々 Breaking Change があったようですが、現状 root がエラー無くレンダリングされることを確認するテストしか行っていないため、特に問題なくアップデートできました。
補足
@babel/plugin-proposal-private-property-in-object
はES2022から@babel/preset-env
に含まれているため、人によっては不要になるかもしれない(?)- https://babeljs.io/docs/babel-plugin-transform-private-property-in-object
- 自分はES2020だったので、まだ必要っぽい(?)
process.env.PUBLIC_URL
に結果が依存するテストを書いてると、テストが落ちるかも。- ejectしたプロジェクトでは、scripts/test.jsで空文字列で初期化されている。
- そもそも結果が環境に依存しないテストに直せるなら直した方がよさそう。
- 最悪、src\setupTests.tsで
process.env.PUBLIC_URL
に必要な初期化を行う。
移行が完了して
ここまで作業したところで、テストがすべて正常に通るようになりました。やったね。
% npm run test > last-origin-unit-viewer@1.2.20 test > jest PASS src/data/unitBasicData.test.ts PASS src/domain/skill/SkillAreaOfEffectMatcher.test.ts PASS src/data/equipmentData.test.ts PASS src/App.test.tsx Test Suites: 4 passed, 4 total Tests: 103 passed, 103 total Snapshots: 0 total Time: 4.972 s, estimated 13 s Ran all test suites.
そして作業がつらすぎたので、Vitestがよさそうな感じだったら移行したい。将来的にViteと一緒に捨てることになっても、その時一番よさそうなテストライブラリに移行すればいいだけなので…。
*1:公式ドキュメントをよく読んでなかったとも言う。
*2:Jest SyntaxError: Unexpected token 'export' · Issue #3443 · react-dnd/react-dnd · GitHub