実現したいこと
Remix で実装したコンポーネントのテストコードを Vitest で書いています.
テスト環境ではバックエンドのサーバを MSW でモックしようとしています.
beforeAll
内で MSW のモックサーバを起動したいです.
発生している問題・分からないこと
beforeAll
が実行されないためモックサーバが起動せず,fetch
でエラーが発生する.
エラーメッセージ
error
1TypeError: fetch failed 2 at node:internal/deps/undici/undici:12500:13 3 at processTicksAndRejections (node:internal/process/task_queues:95:5) 4 at Module.customFetch (/path/to/repository/frontend/client/mutator.ts:5:14) 5 at Module.loader (/path/to/repository/frontend/app/routes/home.users._index/route.tsx:27:19) 6 at loader (/path/to/repository/frontend/test/routes/home.users._index/route.test.tsx:15:11) 7 at /path/to/repository/frontend/node_modules/.pnpm/@remix-run+router@1.19.2/node_modules/@remix-run/router/router.ts:4902:19 8 at callLoaderOrAction (/path/to/repository/frontend/node_modules/.pnpm/@remix-run+router@1.19.2/node_modules/@remix-run/router/router.ts:4966:16) 9 at async Promise.all (index 0) 10 at defaultDataStrategy (/path/to/repository/frontend/node_modules/.pnpm/@remix-run+router@1.19.2/node_modules/@remix-run/router/router.ts:4775:17) 11 at callDataStrategyImpl (/path/to/repository/frontend/node_modules/.pnpm/@remix-run+router@1.19.2/node_modules/@remix-run/router/router.ts:4838:17)
該当のソースコード
vite.config.ts
1/// <reference types="vitest" /> 2import { 3 vitePlugin as remix, 4 cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, 5} from '@remix-run/dev'; 6import { defineConfig } from 'vite'; 7import tsconfigPaths from 'vite-tsconfig-paths'; 8 9export default defineConfig({ 10 plugins: [ 11 remixCloudflareDevProxy(), 12 !process.env.VITEST 13 ? remix({ 14 future: { 15 v3_fetcherPersist: true, 16 v3_relativeSplatPath: true, 17 v3_throwAbortReason: true, 18 }, 19 }) 20 : null, 21 tsconfigPaths(), 22 ], 23 server: { 24 proxy: {}, 25 https: { 26 key: './certs/key.pem', 27 cert: './certs/cert.pem', 28 }, 29 }, 30 test: { 31 alias: { 32 '~/*': './app/*', 33 }, 34 env: { 35 NODE_TLS_REJECT_UNAUTHORIZED: '0', 36 }, 37 environment: 'happy-dom', 38 globals: true, 39 setupFiles: ['./test/setup.ts'], 40 }, 41});
test/setup.ts
1import '@testing-library/jest-dom/vitest'; 2import { cleanup } from '@testing-library/react'; 3import { setupServer } from 'msw/node'; 4import { handlers } from './mocks/handlers'; 5 6const server = setupServer(...handlers); 7 8beforeAll(() => { 9 server.listen({ 10 onUnhandledRequest: 'error', 11 }); 12}); 13 14afterEach(() => { 15 server.resetHandlers(); 16 cleanup(); 17}); 18 19afterAll(() => { 20 server.close(); 21});
app/routes/home.users._index/route.tsx
1import { 2 ActionFunctionArgs, 3 json, 4 LoaderFunctionArgs, 5 redirect, 6} from '@remix-run/cloudflare'; 7import { useLoaderData, useNavigate } from '@remix-run/react'; 8import { deleteUser, getUsers, getUsersResponse } from 'client/client'; 9import UsersListComponent from '~/components/users/UsersListComponent'; 10import { commitSession, getSession } from '~/services/session.server'; 11import { ActionResponse } from '~/types/response'; 12 13interface LoaderData { 14 usersResponse: getUsersResponse; 15 condition: { 16 page?: string; 17 limit?: string; 18 }; 19} 20 21export const loader = async ({ request }: LoaderFunctionArgs) => { 22 // 検索条件を取得する 23 const url = new URL(request.url); 24 const page = url.searchParams.get('page') ?? undefined; 25 const limit = url.searchParams.get('limit') ?? undefined; 26 // ユーザー情報を取得する 27 const response = await getUsers({ page: page, limit: limit }); 28 29 return json<LoaderData>({ 30 usersResponse: response, 31 condition: { 32 page: page, 33 limit: limit, 34 }, 35 }); 36}; 37 38export const action = async ({ request }: ActionFunctionArgs) => { 39 const session = await getSession(request.headers.get('Cookie')); 40 const formData = await request.formData(); 41 const userId = String(formData.get('userId')); 42 43 // 未ログインの場合 44 if (!session.has('user')) { 45 session.flash('error', 'ログインしてください'); 46 return redirect('/login', { 47 headers: { 48 'Set-Cookie': await commitSession(session), 49 }, 50 }); 51 } 52 53 const cookieHeader = [ 54 `__Secure-user_id=${session.get('user')?.id};`, 55 `__Secure-session_token=${session.get('user')?.sessionToken}`, 56 ].join('; '); 57 58 const response = await deleteUser(userId, { 59 headers: { Cookie: cookieHeader }, 60 }); 61 62 switch (response.status) { 63 case 204: 64 session.flash('success', 'ユーザーを削除しました'); 65 if (Number(userId) === session.get('user')?.id) { 66 session.unset('user'); 67 session.flash('success', 'ログアウトしました'); 68 return redirect('/home', { 69 headers: { 70 'Set-Cookie': await commitSession(session), 71 }, 72 }); 73 } 74 break; 75 76 case 401: 77 session.flash('error', 'ログインしてください'); 78 return redirect('/login', { 79 headers: { 80 'Set-Cookie': await commitSession(session), 81 }, 82 }); 83 84 case 404: 85 session.flash('error', 'ユーザーが見つかりませんでした'); 86 break; 87 88 case 500: 89 session.flash('error', 'サーバーエラーが発生しました'); 90 break; 91 } 92 return json<ActionResponse>( 93 { method: 'DELETE', status: response.status }, 94 { 95 headers: { 96 'Set-Cookie': await commitSession(session), 97 }, 98 }, 99 ); 100}; 101 102const UsersListPage = () => { 103 const { usersResponse, condition } = useLoaderData<typeof loader>(); 104 const { page, limit } = condition; 105 const navigate = useNavigate(); 106 107 const handlePaginationChange = (newPage: number) => { 108 const params = new URLSearchParams(); 109 110 if (limit) { 111 params.append('limit', limit); 112 } 113 params.append('page', String(newPage)); 114 115 navigate(`/home/users?${params.toString()}`); 116 }; 117 118 const handleLimitChange = (newLimit: number) => { 119 navigate(`/home/users?limit=${newLimit}`); 120 }; 121 122 return ( 123 <UsersListComponent 124 paginationProps={{ 125 handlePaginationChange: handlePaginationChange, 126 handleLimitChange: handleLimitChange, 127 page: page ? Number(page) : undefined, 128 limit: limit ? Number(limit) : undefined, 129 total: usersResponse.data.totalUser, 130 }} 131 usersResponse={usersResponse} 132 /> 133 ); 134}; 135 136export default UsersListPage;
test/routes/home.user._index/route.test.tsx
1import { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/cloudflare'; 2import { createRemixStub } from '@remix-run/testing'; 3import { screen, waitFor } from '@testing-library/react'; 4import { customRender } from 'test/helpers/wrapper'; 5import UsersListPage, { 6 action, 7 loader, 8} from '~/routes/home.users._index/route'; 9 10const UserListPageStub = createRemixStub([ 11 { 12 path: '/home/users', 13 Component: UsersListPage, 14 async loader({ request }) { 15 return await loader({ request } as LoaderFunctionArgs); 16 }, 17 async action({ request }) { 18 return await action({ request } as ActionFunctionArgs); 19 }, 20 }, 21]); 22 23describe('User List Page', () => { 24 describe('User Table', async () => { 25 const { user } = customRender( 26 <UserListPageStub initialEntries={['/home/users']} />, 27 ); 28 ... 29 }); 30});
試したこと・調べたこと
- teratailやGoogle等で検索した
- ソースコードを自分なりに変更した
- 知人に聞いた
- その他
上記の詳細・結果
setup.ts
を以下のように編集して実行したところ,"setup.ts"
のみが表示されました.setup.ts
自体は実行されているが,beforeAll
は実行されていないようです.
typescript
1console.log('setup.ts') 2 3beforeAll(() => { 4 server.listen({ 5 onUnhandledRequest: 'error', 6 }); 7 console.log('MSW server started.'); 8});
Genspark に beforeAll
が実行されない例を尋ねてみると,beforeAll
内で例外を投げても無視されるという例は見つかりましたが,beforeAll
自体が実行されないという例は見つかりませんでした.
https://github.com/vitest-dev/vitest/issues/1213
補足
特になし
あなたの回答
tips
プレビュー