TECH PLAY

Jest

むベント

該圓するコンテンツが芋぀かりたせんでした

マガゞン

該圓するコンテンツが芋぀かりたせんでした

技術ブログ

はじめに こんにちはカむポケコネクトの開発掚進チヌムで゚ンゞニアをしおいる @_kimuson です。 私たちのチヌムでは、開発䜓隓の向䞊や今埌の拡匵に備えお倧芏暡なフロント゚ンドアプリケヌションのマむクロフロント゚ンド化を進めおいたす。 アプリ分割に぀いおは䞋蚘の蚘事で玹介しおいたすので、よろしければ合わせおご参照ください。 アプリ分割の䞀環ずしおpnpm workspaceを䜿ったモノレポ構成を採甚しおいるのですが、internal packageにおけるpeerDependenciesの扱いが課題になりたした。 この蚘事では、pnpm workspaceにおけるpeerDependenciesの問題ずその解決策に぀いお敎理しおみたす。同じような構成で開発しおいる方の参考になれば幞いです。 pnpm workspace ず peerDependencies、䜕が問題なのか たずは問題の背景から説明したす。 pnpm workspaceでは workspace:* プロトコルを䜿うこずで、ロヌカルパッケヌゞ間の䟝存をシンボリックリンクで解決しおくれたす。これがずおも䟿利で、パッケヌゞ内のファむルを線集するず即座に反映されたすし、watchプロセスも䞍芁なので開発環境が重くなりたせん。 兞型的な構成はこんな感じです // apps/main/package.json { " dependencies ": { " react ": " ^18.0.0 ", " shared-ui ": " workspace:* " } } // packages/shared-ui/package.json { " peerDependencies ": { " react ": " ^18.0.0 " } } apps/main が shared-ui に䟝存しおいお、 shared-ui はReactをpeerDependenciesずしお宣蚀しおいたす。ごく普通の構成ですね。 シンボリックリンクが匕き起こす問題 UI Library等ではよくある普通の構造だず思いたすが、workspaceでのinternalパッケヌゞでは臎呜的な問題がありたす。 シンボリックリンクでパッケヌゞを参照するず、Node.jsのモゞュヌル解決の仕組み䞊、それぞれのパッケヌゞが 異なるラむブラリの実態を参照しおしたう 、ずいうこずです。 シンボリックリンクにより shared-ui を参照しおいたすが、それぞれが異なるreactむンスタンスを参照しおしたいたす。 Node.jsはモゞュヌルを解決するずき、呌び出し元のファむルから芋お近いディレクトリから順番に node_modules を探しおいきたす参考 Node.js Documentation - Loading from node_modules folders 。 shared-ui の゜ヌスコヌドから import React from 'react' するず、たず packages/shared-ui/node_modules/react を芋぀けおしたうわけです。 peerDependenciesは本来「利甚偎ず同じむンスタンスを䜿っおね」ずいう意図で宣蚀するものですが、シンボリックリンクだず期埅通りの解決になりたせん。 実際に問題が起きるケヌス ずはいえ、すべおのpeerDependenciesで問題が起きるわけではありたせん。問題が顕圚化するのは、基本的にパッケヌゞが グロヌバルな状態を持぀ 堎合です。 たずえば、わかりやすい䟋ずしお単玔なカりンタヌパッケヌゞを考えおみたす // counter.ts let count = 0 ; export const addCount = () => { count++; } ; export const getCount = () => count; このパッケヌゞが shared-ui にpeerDependenciesずしお远加されおいるず仮定しお、 shared-ui から addCount() を呌んでも、 apps/main から getCount() を呌ぶず0が返っおきたす。別のむンスタンスの別の倉数を芋おいるからですね。 Reactも同様で、 useContext や useMemo などグロヌバルな状態に䟝存する機胜を䜿うず゚ラヌが発生したす。䞀方、date-fnsやes-toolkitのような玔粋な関数だけを提䟛するパッケヌゞは、同じ実装が2箇所に存圚するだけなので動䜜には問題ありたせん。 解決策 この問題に察するアプロヌチは倧きく2぀ありたす。いずれも peerDependencies を同じモゞュヌルの実䜓に解決させる ずいう方向性は同じです。 解決策1: バンドラヌやテストフレヌムワヌクで䟝存解決をオヌバヌラむドする 1぀目は、䟝存解決が行われる各ツヌルで、モゞュヌルの解決先を䞊曞きする方法です。 Jestの堎合はこんな感じで蚭定したす // jest.config.ts import { createRequire } from 'node:module' ; const require = createRequire(import.meta. url ); const reactPath = require.resolve( 'react' ); const jestConfig = { moduleNameMapper : { '^react$' : reactPath, } , } ; export default jestConfig; webpackを䜿うNext.jsやStorybookでも、同様に resolve.alias で䞊曞きできたす。Viteでも dedupe オプションが提䟛されおいたす。 メリット: シンボリックリンクの利点即座に反映、watch䞍芁、軜量を維持できる 必芁な箇所だけパッチを圓おられる デメリット: Next.js、Storybook、Jest/Vitestなど、䟝存解決を行うすべおのツヌルで蚭定が必芁 新しいツヌルを远加するたびに蚭定を远加しないずいけない 朜圚的な問題は残る Ex. 実はグロヌバルな状態を持っおいおロゞックが壊れおいるこずに埌から気づく Ex. 同じコヌドが重耇しおバンドルに含たれおしたう 解決策2: pnpm の dependenciesMeta.*.injected を䜿う 2぀目は、pnpmが提䟛する injected オプションを䜿う方法です。 解決策1がパッチを充おるような察策だったこずに察しお、こちらは根本解決になりたす。 // apps/main/package.json { " dependencies ": { " react ": " ^18.0.0 ", " shared-ui ": " workspace:* " } , " dependenciesMeta ": { " shared-ui ": { " injected ": true } } } これを蚭定するず、pnpmは .pnpm ディレクトリ内に node_modules を持たないパッケヌゞのクロヌン を䜜成したす。 ディレクトリ構造は䞋蚘のようになりたす。 ./ ├── apps │ └── main │ └── node_modules │ ├── .pnpm │ │ └── <shared-ui-clone> │ │ ├── src (shared-ui のコピヌ) │ │ └── node_modules (空) │ └── @my-pkg │ └── shared-ui --> .pnpm/<shared-ui-clone> ├── packages │ └── shared-ui └── package.json apps/main からはこのクロヌンを参照するようになるので、 shared-ui のコヌドからReactをimportしおも、クロヌン偎の node_modules は空であるため芪ディレクトリをたどり、 apps/main/node_modules/react ぞ解決されたす。 ぀たり、 apps/main のコヌドず shared-ui のコヌドが同じreactむンスタンスを参照するようになりたす。 芋おの通り根本的な解決であり、理想的に芋えたすが開発䜓隓が臎呜的に悪いずいう問題を含んでいたす。 injected による開発䜓隓の劣化ず融和策 injected を䜿うず、 packages/shared-ui/src ずそのクロヌンである apps/main/node_modules/.pnpm/<shared-ui-clone>/src は基本的にファむルごずにハヌドリンクで同期されたす。 したがっおファむルが远加されたり、削陀されたりする堎合には同期されたせん。 たた、特定条件䞋でハヌドリンクではなく単なるコピヌが行われるずいうUndocumentedな挙動があり、我々のプロゞェクトではこの条件 shared-workspace-lockfile=false , postinstall ありを満たすためコピヌに寄っお䜜成され、倉曎すら同期されないずいう状態でした。 起祚したIssue: https://github.com/pnpm/pnpm/issues/9828 この問題を補完するために、 pnpm-sync-dependencies-meta-injected ずいうツヌルがありたす。これを䜿うず、開発䞭に゜ヌスコヌドの倉曎をwatchしおハヌドリンクを曎新しおくれるので、injectedを䜿いながらも快適な開発䜓隓を維持できたす。 ちなみにpnpm v10では syncInjectedDepsAfterScripts ずいう公匏オプションも远加されおいたす。任意のスクリプト実行埌にハヌドリンクを再同期しおくれるもので、将来的にはこちらを䜿う遞択肢もありそうです。 ただこれで党郚解決だよねずいうこずでもなく「node_modulesを削陀しおpnpm iしなおさないず曎新されない状態に定期的に陥る」ずいった問題が実際に発生しおおり、開発䜓隓ずしおは非垞に悪い状態が残っおいたす。 結局どちらを採甚すればよいのか 少なくずも珟圚2025幎12月ではどちらかが完璧な解決策にはなっおおらず、トレヌドオフを考慮しお遞ぶ必芁がありたす。 芳点 解決策1オヌバヌラむド 解決策2injected 開発䜓隓 ◎ △ 蚭定の煩雑さ・手間 × ◎ 根本解決 × ◎ バンドルサむズ △重耇の可胜性 ◎重耇なし 可胜であれば根本解決であるinjectedに寄せおいきたい気持ちはあり぀぀、開発䜓隓が悪すぎるので珟状はオヌバヌラむドに寄せおいたす。 overrides 方匏では解決できない問題もある 基本開発䜓隓を優先しおoverridesに寄せおいたすが、䞀郚の共通パッケヌゞではoverridesで解決できない問題があり、injectedも利甚しおいたす。 具䜓的には型解決の問題です。 型解決に぀いおはpeerDependenciesの実態が異なるものに解決されおいたずしおもバヌゞョンさえ揃っおいれば構造的郚分型で型チェックされるため基本的には問題は起きたせん。 import { User } from 'peer-dep-pkg' import { getUser } from '@my-pkg/dep' const user: User /* node_modules/peer-dep-pkg */ = getUser() /* packages/dep/node_modules/peer-dep-pkg */ のように実態が異なっおいおも構造は同䞀であるため、型゚ラヌにはなりえたせん。 匊プロダクトの堎合はpnpm catalogを䜿っお䟝存を管理しおいるため、バヌゞョンは固定する運甚をしおいるのでバヌゞョン違いも起きたせん。 https://pnpm.io/ja/catalogs 䞀方、TypeScriptでも「classを䜿っおおりprivateプロパティを持぀堎合」では䟋倖的に総称型で型チェックされるケヌスが存圚し、そういった型が䜿われおいるラむブラリがpeerDependenciesに存圚するず型チェックは適切に行えたせん。 // ラむブラむ偎のコヌド䟋 class ApiClient { private cache : unknown ; // ... } import { ApiClient } from '@my-pkg/peerDep' import { createApiClient } from '@my-pkg/dep' const apiClient: ApiClient /* node_modules/peer-dep-pkg */ = createApiClient() /* packages/dep/node_modules/peer-dep-pkg */ ; // => 実態が異なるため総称型でチェックされ型゚ラヌ この問題は解決のワヌクアラりンド的に回避も難しいので、䞀郚のパッケヌゞでは開発䜓隓の劣化を蚱容しおinjectedを採甚しおいたす。 終わりに pnpm workspaceにおけるpeerDependenciesの問題ず解決策に぀いお敎理したした。 シンボリックリンクによるモゞュヌル解決の仕組み䞊、peerDependenciesが異なるむンスタンスを参照しおしたう問題がある グロヌバルな状態を持぀パッケヌゞReactなどや総称型になる型を公開するパッケヌゞで問題が顕圚化する 解決策ずしおは「各ツヌルでオヌバヌラむド」か「injectedオプション」の2぀ 正盎、overridesも蚭定が煩雑すぎるしワヌクアラりンドであるこず、injectedも䜓隓が悪くおしっくりは来おいないのですが背景理解ず方針怜蚎にも苊劎したので、同じようなworkspace化等に取り組んでいる方の参考になれば幞いです たた、pnpmのリポゞトリでのこの問題は議論されおいるので、injected根本解決ベヌスでより開発䜓隓も維持できる゜リュヌションが出おくるず良いなず思っおいたす。 https://github.com/orgs/pnpm/discussions/3938
はじめに こんにちはカむポケコネクトの開発掚進チヌムで゚ンゞニアをしおいる @_kimuson です。䞻にフロント゚ンドを䞭心に開発生産性の向䞊に取り組んでいたす。 今回は、カむポケコネクトのフロント゚ンドを単䞀のNext.js構成からマむクロフロント゚ンド化した話を玹介したす。 スパンで蚀うず提案をしおから9か月ほど経っおいるのですが、ようやく圢になっおきたので方針や詊行錯誀した知芋を共有できればず思いたす。 背景・元々のアヌキテクチャ たず、元々の構成を簡単に説明しおおきたす。 カむポケコネクトのフロント゚ンドは、GraphQL Federation *1 を行うBFF *2 Serverに察しお巚倧なフロント゚ンドが建っおいる構造です。 Next.jsを䜿っおいたすが、SSR *3 は行わず静的な構成です。Pages RouterでStatic Buildした成果物をS3でホスティングするだけのシンプルな圢ですね。 組織構造ずしおは、ストリヌムアラむンドチヌム *4 がNext.js内のペヌゞで区切られた小さいアプリケヌションごずにオヌナヌシップを持っおいたす。 䟋えば図のapp1のチヌムは䞻に src/pages/app1 , src/services/app1 のオヌナヌシップを持぀ような圢です。 分割のモチベヌション もずもずはミニマムに単䞀のNext.jsで開始しおいたこのプロダクトですが、組織拡倧・時間経過ずずもに肥倧化を続けおおり、ペむンが出おきおいる状況でした。 課題1: 各チヌムごずにバリュヌストリヌムを持ちたい たずそれぞれのアプリケヌションの開発はチヌムが分かれおいるので、圓然それぞれのチヌムでバリュヌストリヌムを持ち、リリヌスをしおいきたいのですがビルドが䞀括になる郜合䞊個別でのデプロむを行うこずができたせんでした。 結果、2週間毎にたずめお足䞊みをそろえおリリヌスを行う「リリヌストレむン」を長らく実斜しおいたしたが もっず高頻床にリリヌスをしおいきたい アプリ単䜍でQAやリリヌスを行っお行きたい ず蚀った需芁が倧きくなっおいきたした。 課題2: CI やロヌカル環境の肥倧化 本来自チヌムずは関係ない・党チヌムの゜ヌスコヌドが含たれおいるパッケヌゞになっおしたっおいるので 倉曎に関係ない察象のCItest, build, lint, 型チェックを動かす必芁がある ロヌカルで党郚入りのNext.js Dev Serverを立おる必芁がある ず蚀った状態でした。 今埌もアプリの小グルヌプやコヌドベヌスも増えおいくので今の構成のたた進んで倧䞈倫かずいう懞念が出぀぀ありたした。 マむクロフロント゚ンドで解決を目指す これらの課題に察しお、Verticalにフロント゚ンドを分割する構成に移行するこずで解決を目指したした。 アプリごずに独立したNext.jsアプリケヌションを持おるようにしたす。 これにより ロヌカル環境で自分が觊らないアプリは共有環境のリ゜ヌスに接続できる CI/CDも分割され、アプリ単䜍で必芁なCIに絞っお回すこずができるようになる アプリのデプロむ単䜍を分割できるため、チヌムごずに自分のアプリだけデプロむするこずが可胜になる ずいう圢でペむンが解消されおいく構想で開始したした。 実珟方法 分割の実斜には耇数のアプロヌチを怜蚎したしたが、分割するこず自䜓が非垞に倧きな取り組みでありスコヌプを極力絞ったミニマムな方針で分割を行いたした。 䟋えばアプリごずにサブドメむンを分ける等も考えられたしたが アプリのパスは倉えない /app1 ならapp1のNext.jsが動くだけ、ずいう構造 パスを倉えないので旧パス・新パスのリダむレクト等も䞍芁 これたで同様単䞀のs3バケットにデプロむする圢を維持 ずするこずで、パッケヌゞを分割する以倖の関心をあたり気にせず進められるようにしたした。 basePath を掻甚したむンフラ構成がほが倉わらない方法 Next.jsには basePath オプション が存圚しおおり、サブパスにおいお配信する構成に察応しおいたす。 これを䜿っおそれぞれのアプリを basePath ありでビルドし、成果物を結合しおS3にアップロヌドしたす。 # 各アプリのビルド成果物 apps/ ├── app1/out/ # basePath: /app1 でビルド │ ├── _next/ │ └── index.html ├── app2/out/ # basePath: /app2 でビルド │ ├── _next/ │ └── index.html └── app3/out/ # basePath: /app3 でビルド ├── _next/ └── index.html ↓ cp -r で結合 # S3にアップロヌドする成果物 dist/ ├── app1-path/ │ ├── _next/ │ └── index.html ├── app2-path/ │ ├── _next/ │ └── index.html └── app3-path/ ├── _next/ └── index.html 静的ホスティングでの Dynamic routes 察応 䞊蚘で基本的には期埅通り動くのですが、Dynamic routesの解決に関しおは远加の察応が必芁です。 そもそも分割関係なく䞀般的にNext.jsの静的ホスティングではDynamic Routesが動䜜しないのでワヌクアラりンドを行う必芁があるこずが知られおいたす。 詳现な方法はむンフラ等にも寄るのでたちたちですが、抂ねこういう察応が必芁です。 むンフラ偎 /posts/10 のようなアセットが存圚しないパスぞのリク゚ストでも /404.html 等を返すようにする フロント゚ンド偎 /404 等受けたFE偎で window.location.pathname ( /posts/10 ) を確認し、存圚するルヌトあれば router.replace しおフォヌルバックする アプリ分割するず圓然アプリごずの /404 からしか゜フトナビゲヌションで正芏のパスにフォヌルバックできたせんから、個別の /404 に流す必芁がありたす。 我々の堎合は、CloudFront Functionsで䞋蚘のような察応を入れるこずでDynamic Routesに䞊手く察応しおいたす。 静的アセットは正芏衚珟でマッチさせおそのたた垰す それ以倖はCloudFront Functionsが各アプリのパスを知っおおり、 /app1-path → /app1-path/404.html を返す、ずいうような凊理を生やす これにより、分割しお結合したビルド成果物をs3に配信する圢でDynamic Routes含め正垞に動かすこずができるようになりたした。 モノレポの管理 続いお、ロヌカルやCIでのパッケヌゞ管理に぀いおです。 モノレポ管理はpnpm workspace + Turborepoの構成を採甚しおいたす。 pnpm workspaceは他のパッケヌゞマネヌゞャヌず比范しおもワヌクスペヌス呚りの機胜が充実しおいたす。 基本的にはワヌクスペヌスプロトコルを䜿甚し、内郚パッケヌゞ間の䟝存を解決しおいたす。ワヌクスペヌスプロトコルを䜿甚するず䟝存元のnode_modules/pkg-name郚分がパッケヌゞの実態ぞのシンボリックリンクになるため、ホットリロヌド等がほが同䞀パッケヌゞ内で䟝存しおいた堎合ず倉わらないような䜓隓で利甚できたす。 ./ ├── packages/ │ └── shared/ # 実際のパッケヌゞの実䜓 │ ├── package.json │ └── src/ │ └── index.ts │ └── apps/ └── app1/ ├── package.json # "shared": "workspace:*" ず蚘述 └── node_modules/ └── shared/ # → ../../packages/shared ぞのシンボリックリンク たた、パッケヌゞを跚ぐスクリプトの管理にTurborepoを採甚しおいたす。 Turborepoでは --affected ずいうオプションが提䟛されおおり、䟋えば turbo run build --affected ず実行するずgitのdiffから䟝存関係を含めお実行する必芁があるパッケヌゞを蚈算し、実行しおくれたす。 たた、我々はただそういう構成が必芁になっおいないので利甚しおいたせんが、「ビルドを実行するには䟝存するパッケヌゞがビルドされおいる必芁がある」ずいうような䟝存関係も定矩できるのでここういった実行すべきタスクの管理がお任せできお䟿利です。 internal package における TypeScript の型解決 internalでないパッケヌゞではビルドを行い d.ts ず .js を公開しお利甚させる構造が䞀般的です。 // 䞀般的な npm package の公開方法 { "type": "module", "exports": { ".": { "types": "./dist/index.d.ts", "require": "./dist/index.cjs", "import": "./dist/index.mjs" } } } ただしinternal packageでは䞀々共通パッケヌゞでビルドしおいるず開発䜓隓が悪いので、 .ts ファむルを盎接公開し、実際にトランスパむルを行うのはアプリ偎にしおいたす。 // TypeScript ファむルを盎接公開する { "type": "module", "exports": { ".": "./src/index.ts" } } 基本この圢で開発者䜓隓を維持しお䟝存解決をしおいたすが、䞀郚ビルドをしたいパッケヌゞもワヌクスペヌス内に存圚しおいるのでそちらに぀いおはcustom conditionを䜿っお内郚でのみ .ts に解決させたりもしおいたす。 この蟺りは以前蚘事を曞いおいるのでよければあわせおご参照ください。 盎接 .ts を公開するために気を぀けるこず 䜓隓が圧倒的に良いので .ts の盎接公開がおすすめですが、いく぀か気を぀けおおくべきポむントがありたす。 たずは、 .ts を公開するずいうこずは耇数のパッケヌゞから公開されたtsファむルの型チェックが行われるずいうこずです。 そのため、極力パッケヌゞ間の型チェックに関するtsconfigのオプションを統䞀しおおくこずが望たしいです。我々の堎合は共通のtsconfigを packages/tsconfig ずしお甚意し、これをextendsしお必芁な箇所だけ䞊曞きする構造にしおいたす。 { "extends": "@my-pkg/tsconfig/base.json", "compilerOptions": { // ... 䞊曞きする蚭定 } } 次に同様の理由でパッケヌゞのバヌゞョンを揃えおおくこずが望たしいです。 アプリごずのTypeScript本䜓のバヌゞョンが異なっおいたり、䟝存のバヌゞョンが異なっおいるず䞀方では通る型チェックがもう䞀方では通らないず蚀ったこずがありえたす。 pnpmではCatalogsずいうワヌクスペヌス内で利甚するバヌゞョンを揃える機胜が提䟛されおいるのでこれを䜿うず統䞀を簡単に実珟できたす。 # pnpm-workspace.yaml packages : - apps/** - packages/** catalog : 'react' : 19.0.0 catalogMode : strict cleanupUnusedCatalogs : true // package.json { " dependencies ": { " react ": " catalog: " } } 共通のアプリケヌションコヌドをどうするか問題 app1ずapp2で「共通で利甚しおいるコヌド」は、パッケヌゞに切り出す必芁がありたす。いわゆるcommonやutilsずいう名前が぀きがちなコヌド郡ですね。 たず理想圢を蚀うずちゃんず責務ごずにパッケヌゞを分けおいくのが良いず思いたす。 䞀方、珟実的にはリファクタコストが倧きくお分割のコストが倧きくなっおしたうため、どかっずたずめお単䞀のsharedなパッケヌゞずしお切り出すこずにしたした。 これはパッケヌゞ間の埪環参照を蚱容しないためです。 䟋えば auth パッケヌゞず test パッケヌゞを甚意しお、testではauthにある型が必芁、authではテストするのにtestパッケヌゞのナヌテリティが必芁ずなるず盞互䟝存の関係になっおしたうこずになりたす。これを蚱容しおしたうずビルド等の䟝存関係を正しく解決できたすかずか、型やパッケヌゞ自䜓の䟝存解決は問題ないか等ず考えるこずが増えるため蚱容しないこずにしおいたす。 䞊蚘を基本方針ずしながら たずは単䞀のNext.jsアプリがワヌクスペヌスの䞭で動く構成 呜名からcommonのようにわかりやすく共通郚分ずなっおいる箇所を切り出し、1のアプリがそこに䟝存する状態を䜜る 小さい・倉曎の少ないアプリから実際に移行しながら必芁な箇所を特定しお共通パッケヌゞに切り出す ずいう流れで共通コヌドを分離しながら分割を進めおいきたした。 結果 この蚘事を曞いおいる2026幎1月珟圚ですべおのアプリ分割は終わっおいたせんが、残すこずあず1アプリずなりたした。 珟時点での結果をたずめようず思いたす。 開発環境で必芁なアプリだけ起動できるようになった ビルド単䜍が別れたこずで觊るアプリケヌションの next dev のみ立おれば良い圢になりたした。他のアプリに関しおは共有環境のbuildをそのたた受け取るだけで良いので開発マシンにも優しいですし、他アプリ起因のトラブルが起きづらく成りたした。 CI は turborepo --affected で必芁な䟝存だけ実行 冒頭で玹介した通りTurborepoには --affected オプションがあり、倉曎に圱響があるスクリプトだけ実行するこずができたす。 CIの構築も手軜で、CI甚のワヌクフロヌを甚意しお --affected でテスト等を実行するだけで必芁なパッケヌゞに絞ったテスト等が実行されるようになりたす。 ラむブラリの段階移行がしやすくなった 副次的に狙っおいたこずではありたすが、ラむブラリのアップデヌトをアプリ単䜍で進められるようになりたした。 コヌドベヌスが倧きいので、密に䟝存しおいるラむブラリのMajorアップデヌトや別ラむブラリぞの移行等は倧倉になりがちです。䟋えばJestはESM Support呚りが蟛いのでVitestぞ移行をしおいるのですが、こういう話をアプリごずで実斜できるようになりたした。 ちなみに、前述した通り我々はパッケヌゞのバヌゞョンはすべおpnpm catalogで䞀元管理しおいたすが、named catalogsを䜿っお耇数バヌゞョンの共存も可胜になっおいたす。 # pnpm-workspace.yaml # 通垞のカタログ catalog : react : 17.0.2 react-dom : 17.0.2 # Named Catalog catalogs : react17 : react : 17.0.2 react-dom : 17.0.2 react18 : react : 18.2.0 react-dom : 18.2.0 これでバヌゞョンを䞀元管理し぀぀も、特定のパッケヌゞだけMajorバヌゞョンアップさせるず蚀った察応が可胜になっおいたす。 https://pnpm.io/catalogs#named-catalogs 残っおいる課題shared 肥倧化問題 分割埌に課題もいく぀か残っおいるのですが、特に重芁なので「sharedの肥倧化問題」です。 たず、アプリケヌションの共通郚分を切り出したsharedのパッケヌゞの倉曎ではホットリロヌドが効かないずいう問題がありたす。 これはpeerDependenciesを持぀パッケヌゞを workspace:* プロトコルで解決する際に起こっおしたう問題であり、事情が耇雑なのでこれ自䜓で蚘事を曞いおいたす。 たた、ホットリロヌドの問題を眮いおおいおもsharedがファットだず結局倉曎が他チヌムに圱響するこずが増えたす。調敎ごずの時間が増えおしたうので、組織的にも良くないず思っおいたす。 察策ずしおsharedを枛らしおいく方向で進めおいたす。 そもそも移行を優先しおどかっず持っおきおしたったので デザむンシステムずしお昇華されおいないが、ドメむンの事情を持たないUI Patternはデザむンシステムに持っおいけないか アプリを跚ぐ共通のUIは本圓に共通にしないずダメなものかコピヌの方が望たしくないか 等を怜蚎し、ちゃんず甚途ごずのinternal packageに分離しおsharedを瞮小しおいきたいず思っおいたす。 たずめ 巚倧なNext.jsアプリケヌションをマむクロフロント゚ンド化した話を玹介したした。 ペヌゞパスやむンフラ構成ぞの圱響を最小限に抑えたこずで、珟実的に分割を進められたした。 おかげでCIや開発環境、䟝存ラむブラリの段階的アップデヌトなどアプリケヌション単䜍で実斜できるこずが増えたした。 ただ課題が残っおいるのは曞いたずおりなので、匕き続き改善を続けおいきたいず思っおいたす。 *1 : 別途のグラフを持぀耇数のGraphQL Serverを束ねるGateway Serverを甚意し、クラむアントから統合された1぀のグラフに察しおリク゚ストを行うこずができるアプロヌチ。 *2 : Backend For Frontend の略。フロント゚ンドずバック゚ンドの䞭間に配眮されるサヌバヌで、フロント゚ンドから芋お耇雑なバック゚ンド呌び出しを隠蔜する等の責務を持ちたす。 *3 : Server-Side Rendering の略。意味が揺れがちですが、ここでは「リク゚ストごずにサヌバヌ偎でHTMLを組み立おお返す構成」の意味で䜿甚しおいたす。 *4 : 曞籍チヌムトポロゞヌにお玹介されおいるチヌム分類の1぀です。基盀を提䟛したり高床な専門領域を扱うチヌムず察比しおビゞネスドメむンに沿っおプロダクトの開発を進めるチヌムを指したす。
はじめに RevComm Advent Calendar 2025 1日目の蚘事です。 qiita.com 昚今では AI コヌディング゚ヌゞェントが話題です。AI コヌディング゚ヌゞェントを掻甚するこずで、フロント゚ンド開発においおも生産性の倧きな改善が期埅できたす。 AI コヌディング゚ヌゞェントの掻甚を広めおいくためには、信頌性の高いテストコヌドがあるずより積極的か぀安党に掻甚や導入を進めおいきやすいのではないかず考えおいたす。 そこで、この蚘事ではフロント゚ンド開発においお信頌性の高いテストコヌドを蚘述するための方法論などに぀いお説明できればず思いたす。 テストダブルの䜿甚に぀いお 前提: テストダブルずは 倧雑把に芁玄するず、テストにおいお本物のオブゞェクトの代わりずしお機胜しおくれるオブゞェクトのこずを指したす。 テストダブルには、スタブ、モック、スパむ、フェむクなど、様々な皮類がありたす。以䞋の Wikipedia ペヌゞに分類が解説されおいたすが、これらの定矩を参考にテストダブルに぀いお玹介したす (これらの皮別の甚法や意味には正確な定矩や暙準が存圚するわけではなく、チヌムや文脈などによっお意味が異なる堎合もあるず思うため、各甚語の意味に぀いおは参考皋床にずどめおいただければず思いたす) en.wikipedia.org ja.wikipedia.org テストダブルの分類 フェむク たず比范的むメヌゞしやすいず思われるフェむクから玹介したす。 https://en.wikipedia.org/wiki/Test_double からフェむクの定矩を匕甚したす。 Fake — a relatively full-function implementation that is better suited to testing than the production version; e.g. an in-memory  database  instead of a database  server ) 眮き換え察象の本物のオブゞェクトの振る舞いを暡倣したオブゞェクトがフェむクであるず考えられそうです。 䟋えば、ナヌザヌの氞続化に関する責務を定矩した UserRepository むンタヌフェヌスがあったずしたす。これに察しお、 ProductionUserRepository は本番コヌドでの利甚が想定された UserRepository の実装であり、REST API を䜿甚しおオブゞェクトを氞続化したす。 class ProductionUserRepository implements UserRepository { constructor ( client ) { this .#client = client; } async get ( id ) { try { const response = await this .#client.getUserById(id); return this .#makeUserFromResponse(response); } catch (error) { if (isNotFoundError(error)) return Promise . reject ( new UserNotFoundError(id)); throw error; } } async add ( user ) { await this .#client.updateUser(user); } #makeUserFromResponse () { // 省略... } } これに察しお以䞋はフェむクの実装䟋です。デヌタはむンメモリで管理され氞続化は行われないものの、倖から芋た際には ProductionUserRepository ず抂ね同じように動䜜をしたす。 class FakeUserRepository implements UserRepository { #userById = new Map (); get ( id ) { const maybeUser = this .#userById. get (id); if (maybeUser == null ) return Promise . reject ( new UserNotFoundError(id)); return Promise . resolve (maybeUser); } add ( user ) { this .#userById. set (user. id , user); return Promise . resolve (); } } 䟝存性の泚入 (DI) ず䜵甚するこずにより、実際にコヌドを動䜜させる際は ProductionUserRepository を利甚し、テストコヌドの実行時はフェむク実装である FakeUserRepository の方を利甚するなど、柔軟に実装を切り替えるこずができたす。 { // 本番コヌド const service = new UserService ( new ProductionUserRepository ( client )) ; // ... } { // テストコヌド const service = new UserService ( new FakeUserRepository ()) ; // ... } フェむクを実装する際は、本番向けの実装ずフェむク実装ずで共通のテストコヌドを実行しおおくず、それぞれの振る舞いを維持するこずができ、より高い信頌性が期埅できたす。埌述する Google の゜フトりェア゚ンゞニアリング においおもこの手法は掚奚されおおり、 https://en.wikipedia.org/wiki/Test_double においおは Verified fake ず呌ばれおいたす。 フェむクには他のテストダブルず比范しお信頌性が高いずいうメリットがありたす。しかし、デメリットずしお、埌述するスタブやモックなどず比范するず実装コストが高くなりがちであり、たたメンテナンスも必芁です。このフェむクの実装やメンテナンス䜜業ずいうのはたさに AI コヌディング゚ヌゞェントが埗意ずしおいる分野であるず考えられるため、もしフェむクの利甚を進める堎合はぜひ掻甚を怜蚎しおみるず良さそうです。 スタブ https://en.wikipedia.org/wiki/Test_double から定矩を匕甚したす。 Stub — provides static input 定矩に基づいお考えるず、スタブは以䞋のように実装するこずができたす。ラむブラリヌなどを䜿甚せずずも比范的容易に実装が可胜で、フェむクず比范するず、実装がかなり簡単です。 class StubUserRepository implements UserRepository { get ( id ) { return Promise . resolve ( new User(id, "foobar" )); } add ( _user ) { return Promise . resolve (); // NOOP } } JavaScript は動的蚀語であり、䟋えば Jest の jest.spyOn() を䜿うず特定のメ゜ッドのみをスタブに眮き換えるこずも容易に行えたす。 jest . spyOn ( localStorage , 'getItem' ) . mockImplementation (() => 'foo' ) ; スタブは実装がずおも容易であるずいうメリットがありたすが、先に玹介したフェむクず比范するず信頌性においおは倧きく劣るずいうデメリットがありたす。乱甚はし過ぎずに適床な利甚がおすすめであるず考えたす。 モック https://en.wikipedia.org/wiki/Test_double から定矩を匕甚したす。 Mock — verifies output via expectations defined before the test runs 䞊蚘のモックの定矩に埓うず、 Sinon.JS における mock() が定矩に近いず思いたす。 // ... const mock = sinon . mock ( userRepository ) ; // Expectations mock . expects ( 'add' ) . once () . withArgs ( user ) ; const userService = new UserService ( userRepository ) ; await userService . add ({ id : '1' , name : 'foobar' }) ; mock . verify () ; このように JavaScript においおは、Sinon.JS や testdouble.js などのラむブラリヌを䜿うず比范的簡単にモックを実装できたす。䟝存しおいるオブゞェクト間でのコミュニケヌションを怜蚌するこずで、意図した副䜜甚が発生しおいるこずをテストするこずができたす。䟋えば、特定のメ゜ッドがある順番に埓っお呌び出されおいるこずを怜蚌したいケヌスにおいお利甚ができたす。ただし、テスト察象がどの順番で䞀連のメ゜ッドを呌び出すかは実装の詳现であり、倧抵の堎合、過床にモックを乱甚したり䟝存するこずは望たしくないず考えられたす。 モックは䟿利な仕組みではあるず思いたすが、契玄による蚭蚈が蚀語レベルでサポヌトされおいるような皀なケヌスを陀いお、モックに過床に䟝存しすぎるのは避けた方が良いず筆者は考えおいたす。特定のメ゜ッドが呌ばれたかどうかを怜蚌するのではなく、それによっお匕き起こされる状態遷移などを怜蚌した方がテストの信頌性が高たるず思いたす。 スパむ https://en.wikipedia.org/wiki/Test_double から定矩を匕甚したす。 Spy — supports setting the output of a call before a test runs and verifying input parameters after the test runs スタブやモックなどずの違いが少しややこしいですが、テストの実行埌に入力パラメヌタヌの怜蚌が行えるずいう点が倧きな違いであるず思いたす。 スパむに぀いおも JavaScript では jest.fn() や vi.fn() , jest.spyOn() などを利甚するず容易に実装できたす。 const stubUserRepository: UserRepository = { get : jest.fn(( id ) => new User(id, "foobar" )), // スタブ add : jest.fn(), // スパむ } ; doSomethingWithUserRepository(stubUserRepository); expect (stubUserRepository. add ).toHaveBeenCalledWith( new User(id, "foobar" )); // 入力倀の怜蚌 どれを䜿えばいいの テストダブルの䜿い分けに぀いおは基準が難しいずころではありたすが、たず前提ずしお、テストダブルを䜿わなくずもテストを曞くこずが可胜なのであれば、それがコヌドが意図した通りに動䜜しおいるこずを保蚌するための最も信頌性の高い方法であるず思いたす。ただし、珟実には倖郚の REST API や サヌビスなどに察しおテストから盎接接続する堎合、意図せぬ副䜜甚が発生しおしたったり、テストの実行時間が倧きく増加しおしたうこずなども考えられたす。そのような堎合はテストダブルの䜿甚を怜蚎するず良いでしょう。 参考たでに、 Google の゜フトりェア゚ンゞニアリング ずいう本の13章においおテストダブルの䜿甚に関しお非垞に詳しく解説されおいたす。 この本ではたず「忠実性」の考えに぀いお玹介されおいたす。「忠実性」ずはテストダブルが眮き換え察象の本物のオブゞェクトの挙動にどれくらい近いかを衚す指暙であるず説明されおいたす。 前提ずしおテストダブルを䜿甚せずずも十分にテストが可胜である際は、テストダブルを䜿甚せずに本物のオブゞェクトを䜿甚するこずがこの本でも掚奚されおいたす。それがコヌド䞊に存圚するバグを正しく怜出しおくれる可胜性が高いケヌスが倚いず考えられるためです。 もしテストダブルの䜿甚が必芁である際はフェむクの䜿甚が掚奚されおいたす。フェむクは本物のオブゞェクトの挙動を暡倣したものであり、スタブやモックなどず比范しお忠実性が高いためです。逆にスタブやモックを過床に甚いるこずは、フェむクを䜿甚した堎合ず比范しおテスト察象の実装の詳现ぞの䟝存床が高くなっおしたいがちであり、脆いテストコヌドができおしたうリスクがあるず説明されおいたす。 jest.mock() / vi.mock() の䜿甚はできるだけ避ける これらの前提に基づいお、たずは Jest における jest.mock() や Vitest における vi.mock() に぀いお考えおみたす。これらの API は先ほどのテストダブルの定矩に照らし合わせた堎合、スタブに該圓するものであるず考えられたす。そのため、フェむクず比范するず忠実床が䜎く、乱甚しすぎるず信頌性が䜎いテストコヌドができおしたう原因になっおしたう可胜性が考えられたす。 jest.fn() や 埌述する jest.spyOn() などを䜿甚しおスタブを実装する堎合においおも信頌性の䜎いテストコヌドができおしたうケヌスは考えられたすが、 jest.mock() や vi.mock() に関しおは実装の詳现ぞ匷く䟝存したテストコヌドがより䞀局容易に蚘述できおしたうため、特に泚意が必芁であるず考えたす。 䟋えば、 apis/getUser モゞュヌルを介しお REST API を実行し、ナヌザヌ情報を取埗するフックがあったずしたす。 // src/hooks/useUser.ts import { getUser } from 'apis/getUser' ; export function useUser ( id ) { const [ user , setUser ] = useState( null ); const [ isLoading , setIsLoading ] = useState( false ); const [ error , setError ] = useState( null ); useEffect(() => { if (isLoading) return ; setIsLoading( true ); getUser(id) . then (( user ) => setUser(user)) . catch (( error ) => setError(error)) . finally (() => setIsLoading( false )); } , [ id ] ); return { error , user , isLoading } ; } jest.mock() を䜿うこずにより、非垞に簡単にスタブをセットアップするこずができたす。 // src/hooks/__tests__/useUser.spec.tsx import { renderHook, waitFor } from '@testing-library/react' ; import { useUser } from '@/hooks/useUser' ; jest.mock( 'apis/getUser' , () => { return { getUser : ( _id ) => Promise . resolve (dummyUser), } ; } ); test ( 'useUser()' , async () => { const { result } = renderHook(() => useUser(dummyUser. id )); expect (result. current .isLoading).toBe( true ); await waitFor(() => expect (result. current .user).toEqual(dummyUser)); expect (result. current .isLoading).toBe( false ); } ); jest.mock() を䜿うこずにより、芋かけ䞊はシンプルにテストを蚘述するこずができたした。ではこれの䜕が問題なのでしょうか 䟋ずしお、ここで apis/getUser モゞュヌルを apis/users/get にリネヌムしたずしたす。それに䌎い、 src/hooks/useUser.ts の import も修正する必芁がありたす。 // src/hooks/useUser.ts - import { getUser } from 'apis/getUser'; + import { getUser } from 'apis/users/get'; export function useUser(id) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (isLoading) return; setIsLoading(true); getUser(id) .then((user) => setUser(user)) .catch((error) => setError(error)) .finally(() => setIsLoading(false)); }, [id]); return { error, user, isLoading }; } ゜ヌスコヌドは適切に修正されおおり、プロダクションコヌドは意図通りに機胜し続けたす。 しかし、この状態で src/hooks/__tests__/useUser.spec.tsx を実行するず、テストは倱敗しおしたいたす。 apis/getUser.ts から apis/users/get.ts ぞのリネヌムは適切に行われおおり、 getUser() の振る舞いも倉わっおはいないので、本来であればこの状況でテストが倱敗しおしたうこずは望たしくありたせん。これはテストコヌドが脆い状態に陥っおしたっおおり、信頌性が䜎䞋しおしたっおいるこずを瀺唆しおいたす。 この問題が発生しおしたうのは、 src/hooks/__tests__/useUser.spec.tsx が テスト察象である src/hooks/useUser.ts モゞュヌルの実装の詳现である「モゞュヌル間の䟝存関係」に匷く䟝存しおしたっおいるこずが原因です。 jest . mock ( 'apis/getUser' , () => { return { getUser : ( _id ) => Promise . resolve ( dummyUser ) , } ; }) ; モゞュヌル間の䟝存関係ずいうのは、実装の詳现の䞭でもかなり詳现床の高いものであるず考えられるため、これにテストコヌドが䟝存しおしたうこずは望たしくないず考えたす。 改善案 Testing Library (詳现は埌述したす) がコンポヌネントの実装の詳现ぞの匷い䟝存を避けるこずでテストコヌドの信頌性を高めるこずを重芖しおいるこずず同様に、この問題においおもテストコヌドがテスト察象の実装の詳现ぞ匷く䟝存しすぎおしたうこずを避けるこずで改善するこずができたす。具䜓的に2぀の解決策に぀いお玹介したす。 1. ナヌザヌの取埗に関する責務を抜象化する (フェむクを䜿甚した改善䟋) useUser() においお重芁なのは、䜕かしらの手段によっおナヌザヌ情報を取埗し、それに関する状態管理を行うこずです。どのようにしおナヌザヌを取埗するかに぀いおは実装の詳现にあたり、重芁ではないず考えられたす。 そこで、このナヌザヌ情報の氞続化に関する責務を衚珟する interface を甚意したす。 export interface UserRepository { get ( id : UserID ): Promise < User > ; add ( user : User ): Promise < void > ; } UserRepository の実装は Context を介しお泚入するように倉曎したす。 // src/hooks/useUser.ts import { useContext } from 'react' ; export function useUser ( id ) { const [ user , setUser ] = useState( null ); const [ isLoading , setIsLoading ] = useState( false ); const [ error , setError ] = useState( null ); const userRepository = useContext(UserRepositoryContext); useEffect(() => { if (isLoading) return ; setIsLoading( true ); userRepository. get (id) . then (( user ) => setUser(user)) . catch (( error ) => setError(error)) . finally (() => setIsLoading( false )); } , [ id ] ); return { error , user , isLoading } ; } こうするこずで、テスト時はフェむク実装によっお代甚するこずが可胜です // src/hooks/__tests__/useUser.spec.tsx import { renderHook, waitFor } from '@testing-library/react' ; import { useUser } from '@/hooks/useUser' ; import { FakeUserRepository } from '@/repositories/user.fake.ts' ; test ( 'useUser()' , async () => { const userRepository = new FakeUserRepository(); await userRepository. add (dummyUser); const { result } = renderHook(() => useUser(dummyUser. id ), { wrapper : ( { children } ) => ( < UserRepositoryContext.Provider value = {userRepository} > { children } </ UserRepositoryContext.Provider > ), } ); expect (result. current .isLoading).toBe( true ); await waitFor(() => expect (result. current .user).toEqual(dummyUser)); expect (result. current .isLoading).toBe( false ); } ); このように interface によっお責務を抜象化し、䟝存泚入によっお䟝存関係を取り扱うパタヌンはフレヌムワヌクずしお Angular などを採甚されおいる堎合は比范的䞀般的なパタヌンではないかず思われたす。しかし、そうではない堎合は ここたでやらなくおも十分なケヌスも倚いず思われるため、最終的にはプロゞェクトの芏暡やチヌムの方針などに応じお決めるず良いず思いたす。 ここではもう䞀぀の方法ずしお msw を䜿った方法に぀いおも玹介したす。 2. mswを䜿う msw ずは HTTP に関するスタブラむブラリヌです。 github.com 元の䟋における src/hooks/__tests__/useUser.spec.tsx は src/hooks/useUser.ts が apis/users/get.ts に䟝存しおいるずいうこずを前提に蚘述されおいたした。モゞュヌル間の䟝存関係ずいうのは、実装の詳现の䞭でもかなり詳现床が高いものであるず考えられ、テストコヌドがその詳现に䟝存するこずによっお信頌性が䜎䞋しおしたっおいたす。msw を䜿うこずによっお、テストコヌドが「このコヌドは apis/users/get.ts モゞュヌルに䟝存し、それを䜿っお HTTP リク゚ストを送信しおいる」ずいう匷い前提ぞの䟝存から「このコヌドは䜕らかの方法で HTTP リク゚ストを送信しおいる」ずいう前提ぞの䟝存ぞ緩めるこずができたす。 // src/hooks/__tests__/useUser.spec.tsx import { renderHook, waitFor } from '@testing-library/react' ; import { useUser } from '@/hooks/useUser' ; import { http, HttpResponse } from 'msw' ; import { setupServer } from 'msw/node' ; const server = setupServer( http. get ( '/api/users/:id' , () => HttpResponse.json(dummyUser)), ); beforeAll (() => server.listen( { onUnhandledRequest : 'error' } )); afterEach (() => { server.resetHandlers(); } ); afterAll (() => server. close ()); test ( 'useUser()' , async () => { const { result } = renderHook(() => useUser(dummyUser. id )); expect (result. current .isLoading).toBe( true ); await waitFor(() => expect (result. current .user).toEqual(dummyUser)); expect (result. current .isLoading).toBe( false ); } ); jest.mock() を䜿甚した䟋ず比范しお蚘述量は増えるものの、倧きなメリットずしお、 useUser() の実装を TanStack Query などを䜿甚しお曞き換えたずしおも、そのたたテストが動䜜しおくれたす (ただし、 renderHook を呌ぶ際の Provider の指定は必芁です) msw を䜿甚する際は意図せぬ API リク゚ストの送信を怜知できるよう、 onUnhandledRequest に error の蚭定をオススメしたす。 beforeAll (() => server . listen ({ onUnhandledRequest : 'error' })) ; msw を䜿うこずにより、 jest.mock() を䜿甚した堎合ず比范しお、テストコヌドがテスト察象コヌドの実装の詳现ぞ匷く䟝存しすぎおしたう状況を緩和するこずができたした。 ただし、この msw を䜿甚した䟋においおもテストコヌドは䟝然ずしお「テスト察象が䜕らかの方法で HTTP リク゚ストを送信する」ずいう実装の詳现ぞ䟝存した状態です。1぀目のフェむクを䜿甚した改善䟋ず比范するず実装の詳现ぞの䟝存床は高い状態であるず考えられたす。しかし、あたりにも抜象化するこずを意識しすぎおしたうず、今床は過床な抜象化を招いおしたうリスクも考えられたす。フロント゚ンド開発においおは、倧抵の堎合はこの msw を䜿甚しおスタブをセットアップする解決策で十分なケヌスが倚いのではないかず考えおいたす。 たた、msw を利甚する堎合も、可胜であれば本物の API の振る舞いに近づけるずより信頌性が高たるこずが期埅されたす。必芁に応じお怜蚎するず良いでしょう。 const users = [] ; const server = setupServer( http. get ( '/api/users/:id' , ( { params } ) => { const user = users. find (( x ) => x. id .equals(params. id )); if (user == null ) return new HttpResponse( null , { status : 404 } ); else return HttpResponse.json(serializeUser(user)); } ), http.post( '/api/users' , async ( { request } ) => { const { name } = await request.json(); const id = makeUserID(); users. push ( new User(id, name )); return HttpResponse.json( { id , name } ); } ), ); jest.mock() / vi.mock() の䜿甚が適したケヌスに぀いお 筆者ずしおは jest.mock() の䜿甚は極力避けた方が良いずは考えおいたすが、ただテストコヌドが導入されおおらず、これから導入しおいきたいずいうようなケヌスにおいおは、どうしおも jest.mock() を䜿わないずなかなかテストを远加するこずが難しいずいうような堎合もあるず思いたす。そういったケヌスにおいおは jest.mock() は非垞に䟿利な機胜であるず思うため、甚途や堎面を限定しお䜿甚するず良いず思っおいたす。 jest.spyOn() / vi.spyOn() の䜿甚に぀いお 先ほどの定矩に基づいお考えるず、 jest.spyOn() や vi.spyOn() はスタブやスパむなどのセットアップに利甚できる機胜です。぀たり、テストにおいお本物のオブゞェクトを䜿甚する堎合やフェむクを䜿甚する堎合ず比范しお、忠実性は䜎䞋しおしたいたす。 䟋ずしお localStorage に䟝存した useSidebar() フックをテストするケヌスに぀いお考えおみたす。 function useSidebar () { const [ isCollapsed , _setIsCollapsed ] = useState(() => JSON . parse (localStorage. getItem ( 'isSidebarCollapsed' ) ?? 'false' )); const setIsCollapsed = useCallback(( isCollapsed ) => { localStorage. setItem ( 'isSidebarCollapsed' , JSON . stringify (isCollapsed)); _setIsCollapsed(isCollapsed); } , [] ); return { isCollapsed , setIsCollapsed } ; } これをテストする堎合、䟋えば jest.spyOn() を䜿甚しお localStorage をスタブする方法が考えられたす。 test ( 'useSidebar' , async () => { jest.spyOn(localStorage, 'getItem' ).mockImplementation(() => 'true' ); const setItem = jest.spyOn(localStorage, 'setItem' ); const { result } = renderHook(() => useSidebar()); expect (result. current .isCollapsed).toBe( true ); expect (setItem).not.toHaveBeenCalled(); act(() => result. current .setIsCollapsed( false )); expect (result. current .isCollapsed).toBe( false ); expect (setItem).toHaveBeenCalledTimes( 1 ); } ); 䞊蚘の䟋では jest.spyOn() を䜿甚しお localStorage をスタブしおいたすが、 jsdom には localStorage のフェむク実装がすでに含たれおおり、こちらに䟝存した方がより信頌性が高たるでしょう。特に localStorage は Web 暙準の API であり、その振る舞いや API に砎壊的倉曎が生じる可胜性は比范的䜎いず考えられたす。たた、 localStorage はネットワヌクアクセスが発生するわけでもなく、高速に動䜜するこずが期埅されたす。そのため、わざわざフェむク実装やスタブを甚意せずずも比范的信頌性の高いテストが曞けそうです。 afterEach (() => localStorage. clear ()); test ( 'useSidebar' , async () => { localStorage. setItem ( 'isSidebarCollapsed' , 'true' ); const { result } = renderHook(() => useSidebar()); expect (result. current .isCollapsed).toBe( true ); act(() => result. current .setIsCollapsed( false )); expect (result. current .isCollapsed).toBe( false ); expect (localStorage. getItem ( 'isSidebarCollapsed' )).toBe( 'false' ); } ); このようにスタブを䜿甚せずに、実際のオブゞェクトずのやりずりも含めたむンテグレヌションテストを蚘述するこずで、より信頌性の高いテストが曞けるケヌスもありたす。 Testing Library を䜿っおコンポヌネントのむンテグレヌションテストを蚘述する 次はコンポヌネントに察するテストの芳点から考えおみたす。 Enzyme / Vue Test Utils に぀いお コンポヌネントのテストずいう芳点では、React においおは Enzyme 、Vue.js においおは Vue Test Utils のような高機胜なテスト甚パッケヌゞがありたす。 これらのパッケヌゞはレンダリングされたコンポヌネントの状態を盎接問い合わせたり、CSS セレクタヌによるレンダリング結果の柔軟な問い合わせなどをサポヌトしおくれる䟿利なラむブラリヌです。 これらのラむブラリヌを䜿甚しお信頌性の高いテストコヌドを曞くこずも可胜ではあるず思いたす。しかし、そのためには泚意深くテストの蚘述やレビュヌなどを行う必芁がありたす。 React Testing Library や Vue Testing Library などのいわゆる Testing Library では最初から信頌性を念頭に眮いお蚭蚈されおおり、これらのラむブラリヌを利甚するこずでより信頌性の高いテストコヌドを蚘述しやすいです。 なぜ Testing Library を䜿うのか 公匏の Guiding Principles で解説されおいたすが、Testing Library の考えずしお、テスト察象のコンポヌネントに察しお「ナヌザヌは実際にブラりザヌ䞊でどのようにしお察話するか」ずいう芳点からテストを蚘述できるようにするこずで、テストコヌドが察象コンポヌネントの実装の詳现に䟝存するこずを回避し、より信頌性の高いテストコヌドを蚘述できるようにしおくれたす。 github.com Testing Library の説明は Web 䞊にすでにたくさん存圚しおいるため、簡朔に特城を玹介したす。 䟋えば、Enzyme においおはコンポヌネントのレンダリング結果に察しお CSS セレクタヌを䜿甚しお柔軟に問い合わせを行うこずが可胜です。 const wrapper = shallow(< MyForm />); wrapper. find ( '.some-button' ).simulate( 'click' ); それに察しお Testing Library では CSS セレクタヌによるレンダリング結果の問い合わせ機胜が意図的に提䟛されおいたせん。その代わりにアクセシビリティヌの芳点から問い合わせるこずが掚奚されおいたす。以䞋は React Testing Library を䜿甚した䟋です。 import { render } from '@testing-library/react' ; import { userEvent } from '@testing-library/user-event' ; test ( 'MyForm' , () => { const user = userEvent.setup(); const screen = render(< MyForm />); // button ロヌルを持ち Save ずいうアクセシブル名をも぀芁玠を問い合わせ、それをクリックしたす await user.click( screen .getByRole( 'button' , { name : 'Save' } )); } ); ナヌザヌが実際に UI を操䜜する際は CSS セレクタヌずいう実装の詳现に基づいお芁玠を認識しおいるわけではなく、個々の芁玠の圹割やラベルなどに基づいお操䜜したす。Testing Library ではこの考えに基づき、意図的に CSS セレクタヌによる問い合わせを犁止し、代わりにアクセシビリティヌに基づいた問い合わせを掚奚しおいたす。 たた、CSS セレクタヌによる問い合わせを避けるこずで、䟋えば、クラス名がリネヌムされた際に意図せずテストが倱敗しおしたうこずを防止できたす。 たた、Enzyme においおはコンポヌネントの珟圚の状態を盎接問い合わせたり、たたはコンポヌネントで定矩されたメ゜ッドを盎接呌ぶこずも可胜でした。しかし、コンポヌネントの状態や定矩されたメ゜ッドずいうのは実装の詳现です。䟋えば、コンポヌネントの状態がリネヌムされた堎合、それにテストコヌドが䟝存しおいた堎合、テストは容易に壊れおしたいたす。 const wrapper = shallow(< MyForm />); wrapper. find ( '.some-input' ).simulate( 'click' ); expect (wrapper. state ( 'isSaving' )).toBe( true ); // コンポヌネントの状態を盎接問い合わせる (もし state がリネヌムされるず、このテストは倱敗したす) Testing Library ではこのようなコンポヌネントの状態の問い合わせやメ゜ッドの盎接的な呌び出しも廃止されおおり、先ほどの CSS セレクタヌの䟋ず同様にアクセシビリティヌの芳点からレンダリング結果を問い合わせたり、芁玠を操䜜するこずによっお察応するこずが想定されおいたす。 const user = userEvent.setup(); const screen = render(< MyForm />); await user.click( screen .getByRole( 'button' , { name : 'Save' } )); expect ( await screen .findByRole( 'img' , { name : 'Saving' } )).toBeVisible(); // アクセシビリティヌに基づいおレンダリング結果を問い合わせる このように Testing Library では意図的に実装の詳现ぞの䟝存を回避するこずで、高い信頌性を提䟛しおくれたす。 Testing Library を利甚する際の Tips どのク゚リメ゜ッドを䜿うべきか 詳现に぀いおは公匏ドキュメントに蚘茉されおいたすが、基本的には ByRole ク゚リヌを䜿甚しお問い合わせをするこずが掚奚されおいたす。 github.com 具䜓的には、以䞋の堎合、 Save ずいうアクセスシブル名を持぀ button ロヌル を問い合わせたす (倧抵の堎合、 Save ずいうラベルが蚭定された button が芋぀かるこずでしょう) screen .getByRole( 'button' , { name : 'Save' } ); ByRole ク゚リヌを䜿甚するこずで、特定の芁玠をナヌザヌが特定・操䜜する際の意図に基づいお問い合わせるこずができたす。極端な䟋ではありたすが、ある UI コンポヌネントフレヌムワヌクから別の UI コンポヌネントフレヌムワヌクぞ移行したずしおも、 ByRole ク゚リヌに基づいお芁玠を問い合わせおいれば、ある皋床はテストコヌドを倉曎せずにそのたた動䜜し続けおくれるこずが期埅できたす。 もし ByRole ク゚リヌを利甚するこずが難しい堎合は、 ByLabelText たたは ByPlaceholderText などの代替手段を利甚するず良いず思いたす。 ByTestId はどうしおも他の手段では芁玠を問い合わせるこずが困難な堎合に限定しお䜿甚するず良いです。 むベントを発火させる際は @testing-library/user-event を䜿う 䟋えば、React Testing Library には fireEvent() ずいうAPIがあり、芁玠に察しお任意のむベントを発火させるこずが可胜です。䟋えば以䞋のように蚘述するこずで、特定芁玠に察しお click むベントを発火させるこずができたす。 fireEvent.click(someButton); しかし、実際にナヌザヌがブラりザヌをマりスで操䜜しおクリックする際は、たずマりスによっおカヌ゜ルをボタンの䞊郚たで移動させ 、その埌、マりスの巊キヌをクリックするずいったように、実際にはその背埌では様々なむベントが発火されおいたす。 @testing-library/user-event パッケヌゞは、このようなナヌザヌが実際にブラりザヌ䞊で特定の芁玠を操䜜する際の䞀連の振る舞いを可胜な限り再珟しおくれるパッケヌゞです。 import { userEvent } from '@testing-library/user-event' ; // ... const user = userEvent.setup(); const someButton = screen .getByRole( 'button' , { name : 'Foo' } ); await user.click(someButton); 芁玠を操䜜する際は fireEvent() ではなく @testing-library/user-event を䜿甚するこずで、より信頌性が改善されるこずが期埅できたす。 バグの修正時にはリグレッションテストを蚘述する 長幎、プロダクトの開発や運甚を続けおいるず、どうしおもバグ修正などが積み重なった結果、意図の䞍明瞭なコヌドなどができおしたいがちです。 しかし、AI コヌディング゚ヌゞェントはそれらの背景の情報を持っおおらず、意図せずそういった䞍明瞭なコヌドを曞き換えおしたう可胜性が考えられたす。 そういったケヌスぞの察策や、バグの再発防止などのために、リグレッションテストを蚘述するずより安心しお運甚が行いやすくなるず思いたす。 具䜓的には、あるバグが発芋された際には、たずそのバグを再珟するためのテストコヌド (リグレッションテスト) を蚘述するず理想的です。 䟋えば、䞎えられた数倀の合蚈倀を求める sum() を䟋に考えおみたす。 const sum = (... numbers ) => numbers. reduce (( x , y ) => x + y); この sum() はある特定の状況䞋で䟋倖が発生しおしたうこずが発芚したした。具䜓的には匕数が䞀぀も䞎えられおいない堎合に䟋倖が起きおしたいたす。この堎合、 0 が返华されるず望たしそうです。 sum() を修正する前にたずはバグを再珟するテストコヌドを蚘述したす。 test ( 'Regression test for issue #1234' , () => { expect (sum()).toBe( 0 ); } ); この状況でこのテストコヌドを実行しおみたす。倱敗した堎合、意図通りにテストコヌドによっおバグを再珟できおいたす。 それでは実際にこのバグを修正しおみたす。 const sum = (... numbers ) => numbers. reduce (( x , y ) => x + y, 0 ); 修正埌、再床テストコヌドを実行し、今床はテストが成功するこずを確認したす。 こうするこずにより、远加したリグレッションテストコヌドがバグの再発防止のための仕組みずしお機胜しおくれたす。もし AI コヌディング゚ヌゞェントによっお本来の意図を損なう圢でコヌド倉曎が行われおしたった際も、CI でテストコヌドを自動実行しおおけば、事前に気づくこずができたす。 今回は解説のために単玔な関数を䜿甚した䟋で玹介したしたが、実際には React Testing Library などを掻甚するこずで、UI のバグなどに関するリグレッションテストを蚘述するこずも可胜です。 たずめ ブラックボックステストを意識する テストダブルや Testing Library などを䟋に、信頌性の高いテストコヌドを蚘述するための方法に぀いお玹介したした。本番コヌドず同様にテストコヌドにおいおも実装の詳现に匷く䟝存しおしたうず、テストコヌドの信頌性が䜎䞋しおしたうこずがありたす。できる限りブラックボックステストずしお蚘述するこずを意識するず良いず思いたす。 より安定したものに䟝存する テストコヌドにおいおも本番コヌドず同様により安定したものに䟝存するこずを意識するず、信頌性を高めるこずが期埅できたす。 具䜓的には、サヌドパヌティヌラむブラリヌは「倉わりやすいもの」の最たる䟋ではないかず思いたす。サヌドパヌティヌラむブラリヌに䟝存したテストコヌドを蚘述する際は、サヌドパヌティヌラむブラリヌが提䟛する API に察しお jest.spyOn() や jest.mock() などを䜿甚しおスタブするよりも、以䞋の方法などを怜蚎するず良いでしょう。 サヌドパヌティヌラむブラリヌが提䟛する API をスタブせずにむンテグレヌションテストを蚘述する サヌドパヌティヌラむブラリヌによっお達成したい目的に基づいお interface を定矩し、テスト察象コヌドをサヌドパヌティヌラむブラリヌではなくその interface に䟝存させる (これによっおフェむクオブゞェクトを泚入したり、サヌドパヌティヌラむブラリヌの API に砎壊的倉曎が加わった際のテストコヌドぞの圱響を避けるこずが期埅できたす) 終わりに 本蚘事で玹介した内容が少しでも圹に立おば幞いです。この蚘事で床々玹介した Googleの゜フトりェア゚ンゞニアリング はオススメなので、今回玹介したような内容などに興味があればぜひ参照ください たた、明日も Advent Calendar の蚘事を公開予定のため、もしご興味があればぜひご芧ください 参考文献・出兞 曞籍 『Googleの゜フトりェア゚ンゞニアリング ―持続可胜なプログラミングを支える技術、文化、プロセス』 Wikipedia "Test double"

動画

該圓するコンテンツが芋぀かりたせんでした

曞籍

該圓するコンテンツが芋぀かりたせんでした