kazokmr's Blog

試したこと、読んだこと、見たこと、聴いたことを書きたくなったら書くブログ

フロントエンドアプリ(React)で使うライブラリを最新にしたらテストでエラーになったので色々直した話

モチベーション

自分のフロンエンド技術を維持するため、サンプルアプリを作って試していたり、依存関係のバージョンも最新で動かすようにしている。 だけど最近テスト実行時にこれまで遭遇しなかったエラーが出るようになって対応が難しかった。 一応、テストは全て通るようにはなったけど、結構大掛かりな対応になりそうなので、まとめておく。

多分、内容は更新していくかm

エラーになったpackage.json

サンプルアプリケーションとエラーとなった時点のpackage.json を基点として記載しておく

github.com

{
  "name": "project-frontend-dev-process",
  "version": "0.1.0",
  "private": true,
  "license": "MIT",
  "engines": {
    "node": "16.x"
  },
  "packageManager": "yarn@1.22.19",
  "dependencies": {
    "@emotion/react": "^11.9.3",
    "@emotion/styled": "^11.9.3",
    "@fontsource/roboto": "^4.5.7",
    "@mui/icons-material": "^5.8.4",
    "@mui/material": "^5.9.1",
    "@tanstack/react-query": "^4.0.10",
    "@tanstack/react-query-devtools": "^4.0.10",
    "axios": "^0.27.2",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "recoil": "^0.7.4",
    "ulid": "^2.3.0",
    "web-vitals": "^2.1.4"
  },
  "devDependencies": {
    "@storybook/addon-a11y": "^6.5.9",
    "@storybook/addon-actions": "^6.5.9",
    "@storybook/addon-essentials": "^6.5.9",
    "@storybook/addon-interactions": "^6.5.9",
    "@storybook/addon-links": "^6.5.9",
    "@storybook/addon-storyshots": "^6.5.9",
    "@storybook/builder-webpack5": "^6.5.9",
    "@storybook/jest": "^0.0.10",
    "@storybook/manager-webpack5": "^6.5.9",
    "@storybook/node-logger": "^6.5.9",
    "@storybook/preset-create-react-app": "^4.1.2",
    "@storybook/react": "^6.5.9",
    "@storybook/testing-library": "^0.0.13",
    "@storybook/testing-react": "^1.3.0",
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/react": "^13.3.0",
    "@testing-library/react-hooks": "^8.0.1",
    "@testing-library/user-event": "^14.2.6",
    "@types/jest": "^28.1.6",
    "@types/node": "^18.0.6",
    "@types/react": "^18.0.15",
    "@types/react-dom": "^18.0.6",
    "chromatic": "^6.7.1",
    "jest": "^28.1.3",
    "jest-environment-jsdom": "^28.1.3",
    "msw": "^0.44.2",
    "msw-storybook-addon": "^1.6.3",
    "prettier": "^2.6.2",
    "react-scripts": "^5.0.1",
    "ts-jest": "^28.0.7",
    "typescript": "^4.7.4",
    "webpack": "^5.73.0"
  },
  "resolutions": {
    "react-test-renderer": "^18.2.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ],
    "overrides": [
      {
        "files": [
          "**/*.stories.*"
        ],
        "rules": {
          "import/no-anonymous-default-export": "off"
        }
      }
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "jest": {
    "testMatch": [
      "**/__tests__/**/*.test.[tj]s?(x)"
    ],
    "transform": {
      "^.+\\.(ts|tsx)$": "ts-jest"
    }
  },
  "msw": {
    "workerDirectory": "public"
  },
  "homepage": "."
}

起きたこと

Serverアクセスする所でNetworkエラーが発生した

MSWを0.42.1 までだと発生しない。 だけど設定してあるhandlerに対しても、handleしていないと出てはいた

ServerのリクエストURLにホストをつけた

https://example.com を付けるようにした mswのHandlerもこのホストで受け付けるようにした

結果、http://localhost CrossOrigin エラーが発生した

Jestの設定に testEnvironmentを指定しようとした

react-scriptではこの変数に対応していないことを知る

ここでreact-scriptsのJestがV27なことに気づく

v28を使いたいのとこの辺がのversion違いがエラーの原因な気がしたので、scriptsのtestコマンドを react-scripts を介さずに "test": "jest" に変えた

そうしたら他のテストでもエラーが発生し始め、全体的にテストが通らなくなってきたので、見直すことにした

jest.config.ts と jest.setup.ts を作る

jest.setup.ts は CreateReactAppが作成していた、src/setupTests.js を 名前を変えただけ。 rootに持ってきたのは jest.config.tsと並べて管理したかったから

ちなみにjest.setup.jsだとimport moduleのエラーが出てしまい、 import "@testing-library/jest-dom" が利かず、jest-dom のライブラリを利用している テストファイルごとに付けなければいけなくなった。 これを.tsに変えると直るのだが、モジュールシステムがわからない。

jest.configの方も以下を参考に .ts に変えた。 この過程で ts-node をインストールしている

kulshekhar.github.io

IntelliJでテストを単独実行すると 前述の jest.setup.js をみてくれない

CLIではなく、IDE上で特定のケースのテストを実行しようとする以下のようなエラーが出る

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

toBeInTheDocument() は、jest-domに含まれているので、setupTests.ts が参照されていないのではと考えてConfigurationを見たら Jest packageが、react-scripts を参照していたので、これを jest に変えたら通った。 Documentの内容から、react-scripts を使って実行するのが最優先になっているのだと思った。 なので react-scripts をremove を考え始めている

www.jetbrains.com

Mockの状態をクリアする必要が出てきた

Mockを使って toHaveBeenCalled() で検証していた箇所が軒並みFailedになり始めた。 テストを繰り返し行う場合や実行順で影響が出るので、テストごとに状態がクリアされていないと判断し、クリア処理を追加した

beforeEach(() => mockedMutate.mockClear());

jestjs.io

今までこれが無くてもテストが通っていたのは、 react-scripts が何かをおこなっていたからだと予測できるが、深く追ってはいない。

Storyshots.test が失敗する

Storybookのアドオンを利用したSnapshotテストがエラーを返していた。

 FAIL  src/__tests__/Storyshots.test.ts
  ● 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:

    /Users/kazokmr/IdeaProjects/project-frontend-dev-process/.storybook/preview.js:3
    import "../src/index.css";
    ^^^^^^

    SyntaxError: Cannot use import statement outside a module

jest.setup.js の import "@testing-library/jest-dom" で出力されたエラーと同じだったので、 ./.storybook/preview.js を .ts に変えたらエラーが変わった

Details:

    /Users/kazokmr/IdeaProjects/project-frontend-dev-process/src/index.css:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){body {
                                                                                           ^

    SyntaxError: Unexpected token '{'

zenn.dev

こちらの記事を参考に ./.jest/style.ts 作って以下のように1行追記し、 jest.config.ts の moduleNameMapperプロパティで cssファイルをこのファイルにマッピングするようにした

export default {};
...

  moduleNameMapper: {
    "\\.(css|less)$": "<rootDir>/.jest/style.ts"
  }
...

一応これでエラーはなくなり、Snapshotも取得できるように戻った。

ただし、以下のようなエラーメッセージは出ているので、react-scriptsを介さない場合の設定に見直す必要はあると思う。 .mdxファイルに対しても同じ対処が必要だった。

 ● Console

    console.error
      Unexpected error while loading ./stories/Introduction.stories.mdx: /Users/kazokmr/IdeaProjects/project-frontend-dev-process/src/stories/Introduction.stories.mdx: Support for the experimental syntax 'jsx' isn't currently enabled (11:1):
      
         9 | import StackAlt from './assets/stackalt.svg';
        10 |
      > 11 | <Meta title="Example/Introduction" />
           | ^
        12 |
        13 | <style>{`
        14 |   .subheading {
      
      Add @babel/preset-react (https://github.com/babel/babel/tree/main/packages/babel-preset-react) to the 'presets' section of your Babel config to enable transformation.
      If you want to leave it as-is, add @babel/plugin-syntax-jsx (https://github.com/babel/babel/tree/main/packages/babel-plugin-syntax-jsx) to the 'plugins' section to enable parsing.
       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:
      
      SyntaxError: /Users/kazokmr/IdeaProjects/project-frontend-dev-process/src/stories/Introduction.stories.mdx: Support for the experimental syntax 'jsx' isn't currently enabled (11:1):
      
         9 | import StackAlt from './assets/stackalt.svg';
        10 |
      > 11 | <Meta title="Example/Introduction" />
           | ^
        12 |
        13 | <style>{`
        14 |   .subheading {
      
      Add @babel/preset-react (https://github.com/babel/babel/tree/main/packages/babel-preset-react) to the 'presets' section of your Babel config to enable transformation.
      If you want to leave it as-is, add @babel/plugin-syntax-jsx (https://github.com/babel/babel/tree/main/packages/babel-plugin-syntax-jsx) to the 'plugins' section to enable parsing.
          at instantiate (/Users/kazokmr/IdeaProjects/project-frontend-dev-process/node_modules/@babel/parser/src/parse-error/credentials.js:61:22)
          at instantiate (/Users/kazokmr/IdeaProjects/project-frontend-dev-process/node_modules/@babel/parser/src/parse-error.js:58:12)
          at Parser.toParseError [as raise] (/Users/kazokmr/IdeaProjects/project-frontend-dev-process/node_modules/@babel/parser/src/tokenizer/index.js:1736:19)
          at Parser.raise [as expectOnePlugin] (/Users/kazokmr/IdeaProjects/project-frontend-dev-process/node_modules/@babel/parser/src/tokenizer/index.js:1800:18)
          at Parser.expectOnePlugin [as parseExprAtom] (/Users/kazokmr/IdeaProjects/project-frontend-dev-process/node_modules/@babel/parser/src/parser/expression.js:1239:16)
          at Parser.parseExprAtom [as parseExprSubscripts] (/Users/kazokmr/IdeaProjects/project-frontend-dev-process/node_modules/@babel/parser/src/parser/expression.js:684:23)
          at Parser.parseExprSubscripts [as parseUpdate] (/Users/kazokmr/IdeaProjects/project-frontend-dev-process/node_modules/@babel/parser/src/parser/expression.js:663:21)
          at Parser.parseUpdate [as parseMaybeUnary] (/Users/kazokmr/IdeaProjects/project-frontend-dev-process/node_modules/@babel/parser/src/parser/expression.js:632:23)
          at Parser.parseMaybeUnary [as parseMaybeUnaryOrPrivate] (/Users/kazokmr/IdeaProjects/project-frontend-dev-process/node_modules/@babel/parser/src/parser/expression.js:384:14)
          at Parser.parseMaybeUnaryOrPrivate [as parseExprOps] (/Users/kazokmr/IdeaProjects/project-frontend-dev-process/node_modules/@babel/parser/src/parser/expression.js:394:23)

      at Object.error (node_modules/@storybook/client-logger/dist/cjs/index.js:74:67)
      at node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:84:32
          at Array.forEach (<anonymous>)
      at node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:77:18
          at Array.forEach (<anonymous>)
      at executeLoadable (node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:76:10)
      at executeLoadableForChanges (node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:127:20)
      at Object.getProjectAnnotations [as nextFn] (node_modules/@storybook/core-client/dist/cjs/preview/start.js:161:84)

モジュールシステム も react-scripts も tsc も何にもわかっていないなぁ自分。

(7.26 追記) 上記の問題は Storyshotsでスナップショットテストを実行する際に、.cssや .mdx ファイルの解析に失敗していることが問題だった。 なので対応としては大きく2つの方法があり、今回はテスト自体への支障が無いと判断して、2の対応をしたことになる

  1. Jest実行時に解析できるようにファイルの中身を変換する (jest.config の transformオプションを使う)
  2. Jest実行時にファイルの参照先をモックファイルに変換して無視させる (jest.config の moduleNameMapperオプションを使う)

1は変換するためのモジュールを予めインストールしておき、そのモジュールを指定すると良さそう。 例えば、CSS なら jest-css-modules-transform, MDXなら @storybook/addon-docs/jest-transform-mdx などを使うのが良さそう。と思ったけど、CSSの方はそれでも行けたけど、MDXは追加したモジュールでエラーが出た。

このあと調べること

テストは通っているので、これでコミットはするが大きく2つやるべきことが残っている

snapshotsテストの設定見直し

実行ログではエラーが出ていたり、一部 react-scriptsに依存した設定になっているように見受けられるので調査・対応する。

多分だけど、Storybook全体で設定内容を見直した方が良さそう

(7.26 追記) 前述の通りMDXファイルの解析エラーが問題だったので、今回はCSSの時と同様にモック化したMDXファイルに置き換える対応で解決した。

Viteに移行する

jestなど react-scripts でバンドルしているものは、最新バージョンに追従するのが難しいので、react-scripts (CreateReactApp)からの脱却を考えている。できればライブラリへの依存度が少ない薄いビルドツールを使いたい。

となると vite が候補に挙げられるのでこれを試してみたい