-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mock Service Workerについて検証する #1491
Comments
mswで画像をmockしたい場合の実装方法msw2系になった際にだいぶ変わったらしい。
よく出てくるreq, res, ctxのパターンが非推奨になっているのでこの形のサンプルコードはうまく使えない。 rest.get('/resource', (req, res, ctx) => {})
http.get('/resource', (info) => {}) 公式ドキュメント通りに画像用のハンドラーを作成したが、エラーで落ちてしまう。
import { HttpResponse, http } from 'msw';
export const assetsHandlers = [
http.get('/api/assets/:assetCode', async ({ params }) => {
const imageBuffer = await fetch(`/mock/images/${params.assetCode}`).then(
(response) => response.arrayBuffer(),
);
return HttpResponse.arrayBuffer(imageBuffer, {
status: 200,
headers: {
'Content-Type': 'image/png',
},
});
}),
];
下記を設定しておかないとモックを作っていない箇所に対して警告が大量に出るので入れておく worker.start({
onUnhandledRequest: 'bypass',
}); |
mockServiceWorker.jsを本番ではpublicフォルダに置きたくない/public 配下にmockServiceWorker.jsを置く必要があるが、 バンドルされるコードに関しては、 viteのpluginを使用する方法がある。
下記の実装でそれらしき動きをするようになったが、
function excludeMsw(): Plugin {
return {
name: 'exclude-msw',
resolveId: (source) => {
return source === 'virtual-module' ? source : null;
},
renderStart() {
const outDir = './public';
const msWorker = path.resolve(outDir, 'mockServiceWorker.js');
fs.rm(msWorker, () => console.log(`Deleted ${msWorker}`));
},
};
} resolveIdの分岐元々デフォルトではresolveIdがresolveIdを呼び出し無限ループになるので、 buildStart⇒build-hooks |
productionコードに含まれると判断されるので、
|
mswのワーカープロセスが立ち上がるよりも先にアプリがレンダリングされるとエラーが出てしまう。 ワーカーが起動するまでレンダリングを延期するように設定する必要がある?
下記のように修正。
async function enableMocking() {
if (import.meta.env.MODE !== 'mock') {
return;
}
const { worker } = await import('../mock/browser');
// eslint-disable-next-line consistent-return
return worker.start({
onUnhandledRequest: 'bypass',
});
}
enableMocking().then(() => {
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(router);
app.use(errorHandlerPlugin);
authenticationGuard(router);
app.mount('#app');
}); アドホックだが、下記でlintエラーにはならなくなる。 async function enableMocking(): Promise<ServiceWorkerRegistration | undefined> {
if (import.meta.env.MODE !== 'mock') {
return undefined;
}
const { worker } = await import('../mock/browser');
return worker.start({
onUnhandledRequest: 'bypass',
});
} |
Viteのサーバーオプションとの比較ハンドラー
MSWexport const catalogItemsHandlers = [
http.get('/api/catalog-items', () => {
return HttpResponse.json(pagedListCatalogItem, { status: 200 });
}),
http.post('/api/catalog-items', () => {
return new HttpResponse(null, { status: 201 });
}),
http.get<
GetCatalogItemParams,
never,
never,
'/api/catalog-items/:catalogItemId'
>('/api/catalog-items/:catalogItemId', async ({ params }) => {
const { catalogItemId } = params;
const item = catalogItems.find(
(items) => items.id === Number(catalogItemId),
);
return HttpResponse.json(item, { status: 200 });
}),
http.delete('/api/catalog-items/:catalogItemId', () => {
return new HttpResponse(null, { status: 204 });
}),
http.put('/api/catalog-items/:catalogItemId', () => {
return new HttpResponse(null, { status: 204 });
}),
]; Viteexport const basketApiMock = (middlewares: Connect.Server) => {
middlewares.use(`/${base}/basket-items`, (req, res) => {
if (req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(JSON.stringify(basket));
res.end();
return;
}
if (req.method === 'DELETE') {
const catalogItemId = Number(req.url?.substring(1));
res.writeHead(204, { 'Content-Type': 'application/json' });
basket.basketItems = basket.basketItems?.filter(
(item) => item.catalogItemId !== catalogItemId,
);
calcBasketAccount();
res.end();
return;
}
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
if (req.method === 'POST') {
const dto: PostBasketItemsRequest = JSON.parse(body);
const target = basket.basketItems?.filter(
(item) => item.catalogItemId === dto.catalogItemId,
);
if (target) {
if (target && target.length === 0) {
const addBasketItem = mockBasketItems.find(
(item) => item.catalogItemId === dto.catalogItemId,
);
if (typeof addBasketItem !== 'undefined') {
addBasketItem.quantity = dto.addedQuantity ?? 0;
basket.basketItems?.push(addBasketItem);
}
} else {
target[0].quantity += dto.addedQuantity ?? 0;
}
}
calcBasketAccount();
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end();
return;
}
if (req.method === 'PUT') {
const dto: PutBasketItemsRequest[] = JSON.parse(body);
dto.forEach((putBasketItem) => {
const target = basket.basketItems?.filter(
(item) => item.catalogItemId === putBasketItem.catalogItemId,
);
if (target) {
if (target.length === 0) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end();
} else {
target[0].quantity = putBasketItem.quantity;
}
}
});
calcBasketAccount();
res.writeHead(204, { 'Content-Type': 'application/json' });
res.end();
}
});
});
}; クエリパラメータの読み込みexport const assetsHandlers = [
http.get('/api/assets/:assetCode', async ({ params }) => {
const imageBuffer = await fetch(
`/mock/images/${params.assetCode}.png`,
).then((response) => response.arrayBuffer());
return HttpResponse.arrayBuffer(imageBuffer, {
status: 200,
headers: {
'Content-Type': 'image/png',
},
});
}),
]; |
問題点依存するパッケージが増えること以外に、下記の問題点が判明している。 mockServiceWorker.jsをprodビルド時に削除する必要がある
function excludeMsw(): Plugin {
return {
name: 'exclude-msw',
resolveId: (source) => {
return source === 'virtual-module' ? source : null;
},
renderStart() {
const outDir = './public';
const msWorker = path.resolve(outDir, 'mockServiceWorker.js');
// eslint-disable-next-line no-console
fs.rm(msWorker, () => console.log(`Deleted ${msWorker}`));
},
};
}
…
plugins: mode === 'prod' ? [...plugins, excludeMsw()] : plugins, ワーカーの起動とアプリのレンダリングの順序の制御が必要
async function enableMocking(): Promise<ServiceWorkerRegistration | undefined> {
if (import.meta.env.MODE !== 'mock') {
return undefined;
}
const { worker } = await import('../mock/browser');
return worker.start({
onUnhandledRequest: 'bypass',
});
}
enableMocking().then(() => {
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(router);
app.use(errorHandlerPlugin);
authenticationGuard(router);
app.mount('#app');
}); |
その他Vitest2系で、experimentalだが、コンポーネントテスト用にブラウザーモードという機能が追加された。 |
VitestのVSCODE拡張機能Vitest公式のプラグインが存在し、テストタブから操作できる。
|
テストケース作成時の注意点Pinia各ケースの開始時にPiniaを初期化しておく必要がある。 初期化しておかないと、エラー beforeEach(() => {
setActivePinia(createPinia())
}) vue-routervue-routerを使う箇所をテストする場合、
未対応の場合はrouterを使用する箇所でエラーになる。
import { router } from "@/router"
const wrapper = mount(App,{global:{plugins: [router]}});
await router.isReady() |
ブラウザーモードと併用する場合、nodeのほうを使うかbrowserのほうを使うかを動的に設定する必要がある。 |
↓のissueに起票 コンポーネント外でのstoreの使用都度関数内でuseStoreを呼び出さないとエラーになるように見える。
app.use(pinia)によってpiniaプラグインがインストールされる前にuseStoreがコールされると、 これが正しければ、既存のserviceクラスの実装を見直したほうがいいのかもしれない。 import { createRouter } from 'vue-router'
const router = createRouter({
// ...
})
// ❌ Depending on the order of imports this will fail
const store = useStore()
router.beforeEach((to, from, next) => {
// we wanted to use the store here
if (store.isLoggedIn) next()
else next('/login')
})
router.beforeEach((to) => {
// ✅ This will work because the router starts its navigation after
// the router is installed and pinia will be installed too
const store = useStore()
if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})
動作しないconst catalogStore = useCatalogStore();
export async function fetchCategoriesAndBrands() {
await catalogStore.fetchCategories();
await catalogStore.fetchBrands();
}
export async function fetchItems(categoryId: number, brandsId: number) {
await catalogStore.fetchItems(categoryId, brandsId);
} 動作するexport async function fetchCategoriesAndBrands() {
const catalogStore = useCatalogStore();
await catalogStore.fetchCategories();
await catalogStore.fetchBrands();
}
export async function fetchItems(categoryId: number, brandsId: number) {
const catalogStore = useCatalogStore();
await catalogStore.fetchItems(categoryId, brandsId);
} |
ローカルに別のcloneを行ったところ謎のエラーでviteの開発サーバーが上がらなくなったが、 |
非同期操作非同期操作をテストする場合には APIコールするたびに呼ばなければ順序がおかしくなるので忘れないようにするのが結構しんどい。 describe('アイテム追加画面', () => {
it('アイテムを追加できる', async () => {
const pinia = createPinia();
setActivePinia(pinia);
const wrapper = mount(ItemsAddView, { global: { plugins: [pinia,router] } });
await flushPromises()
expect(wrapper.html()).toContain('カタログアイテム追加');
await wrapper.find('button').trigger('click');
await flushPromises()
expect(wrapper.html()).toContain('カタログアイテムを追加しました。');
});
}); |
関数単体テストVitestを使用、helper関数などが対象 実装例コンポーネント単体テストでcurrencyHelperをimportしているので、間接的にテストされている UIコンポーネント単体テストVitestを使用 実装例
UIコンポーネント結合テストVitestを使用 E2EテストCypressを使用
|
やりたいこと最終的には、フロントエンドの自動テストのガイドラインを作りたい
ことを鑑みつつ、
Atomic Designに従う場合のcomponent配下
|
Viteset2.1の新機能ブラウザーモードで使用することを想定したロケーターAPIなるものが追加された。
|
概要
Mock Service Worker(MSW)で既存のViteのサーバーオプションを使用したモックと同等の機能を持つモックを実装し、比較する。
詳細
#975 から派生
完了条件
The text was updated successfully, but these errors were encountered: