- TOP
- ã¿ã°äžèЧ
- Jest
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"
åç»
該åœããã³ã³ãã³ããèŠã€ãããŸããã§ãã
æžç±
該åœããã³ã³ãã³ããèŠã€ãããŸããã§ãã







