👻

Nuxt3 で msw を使ったモックをしてみる

2023/04/09に公開
4

概要

最近フロントエンドテスト関連の記事でよく目にする msw を Nuxt3 に導入してみた手順を記事にしました。

サンプルリポジトリ
https://github.com/harusame0616/nuxt3-msw

環境

名前 バージョン
OS Mac(M1) Ventura 13.0
Yarn 1.22.19
Node 16.19.1
Nuxt 3.3.3
msw 1.2.1

手順

Nuxt 設定

Nuxt3 プロジェクト作成

npx nuxi init nuxt3-msw

runtimeConfig に baseUrl 設定

$fetch に設定する baseUrl を runtimeConfig に設定します。
値自体は環境変数に NUXT_PUBLIC_BASE_URL に値を設定すると自動で上書きしてくれるので、実行時に値を指定することにします。

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      baseURL: '', // 環境変数の NUXT_PUBLIC_BASE_URL で設定
    }
  },
});

msw 設定

インストール

Mock Service Worker: Install

npm install msw --save-dev
# or
yarn add msw --dev

モックハンドラの準備

Mock Service Worker: Define mocks
モック用のディレクトリを準備します。

mkdir mocks

モックを処理するハンドラファイルを作成します。

RestAPI では メソッドと URL パターンによってハンドラを定義できます。
GraphQL ではオペレーション種類(query/mutation) とオペレーション名によってハンドラを定義できます。

今回は /api/books に GET リクエストが投げられたときに書籍リストを返すような REST API を想定してモックしてみます。

msw では共通の baseUrl を設定する方法がないので、パスとベース URL を合成する関数を定義して URL パターンを設定しています。

mocks/handlers.ts
import { rest } from "msw";

const baseURL = (path: string) =>
  new URL(path, process.env.NUXT_PUBLIC_BASE_URL).toString();

export const handlers = [
  rest.get(baseURL("/api/books"), (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({
        books: [
          { id: 1, title: "雨ニモマケズ", author: "宮沢 賢治" },
          { id: 2, title: "走れメロス", author: "太宰 治" },
          { id: 3, title: "こころ", author: "夏目 漱石" },
        ],
      })
    );
  }),
];

サーバー/サービスワーカー設定

msw は browser でも node でも動作しますが、それぞれで設定方法が異なります。
Nuxt の場合は SSR 側で node 用の設定、 CSR 側で browser 用の設定が必要になります。
(SSR が無効の場合は browser の設定のみ)

node (SSR 用)
mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);
browser(CSR 用)

以下のコマンドで public ディレクトリにモック用のサービスワーカーを作成します。

npx msw init public/ --save
/mocks/browser.ts
import { setupWorker } from "msw";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);

Nuxt への組み込み

msw の設定ができたので Nuxt 用に plugin を作成して Nuxt に組み込みます。

plugins ディレクトリにファイルを作成すれば Nuxt 実行時に自動で読み込まれます。
また、suffix に .server をつけると SSR 時のみ、 .client をつけると CSR 時のみ実行されます。

plugins/msw.server.ts
import { server } from "~/mocks/node";

export default defineNuxtPlugin(() => {
  server.listen();
});
// plugins/msw.client.ts
import { worker } from "~/mocks/browser";

export default defineNuxtPlugin(async () => {
  await worker.start();
});

動作確認

SSR 時と CSR 時にモックされるかを確認するため 2 ページ作成します。

  • pages/index.vue
    • pages/books へのリンクのみのページ
  • pages/books.vue
    • API をコールして画面に books を表示するページ
pages/index.vue
<template>
  <NuxtLink to="books"> books </NuxtLink>
</template>
pages/books.vue
<script setup lang="ts">
const config = useRuntimeConfig();

const { data: books } = await useFetch<{
  books: { id: number; title: string; author: string }[];
}>("/api/books", {
  baseURL: config.public.baseUrl,
});
</script>

<template>
  <main>
    <h1>Books</h1>
    <ul>
      <li v-for="{ id, title, author } of books?.books" :key="id">
        {{ title }} ({{ author }})
      </li>
    </ul>
  </main>
</template>

また、pages の内容が見られるように app.vue に <NuxtPage /> を追加します。

<template>
  <NuxtPage />
</template>

実行確認

baseUrl 用に環境変数を指定して、Nuxt の dev サーバーを起動します。

NUXT_PUBLIC_BASE_URL=http://apiserver/ yarn dev

まずは CSR の動作確認するため、 http://localhost:3000/ にアクセスして、books へのリンクをクリックし、正しくモックされているか確認します。
またその後直接 http://localhost:3000/books にアクセスしモックされているか確認します。

上記どちらも確認できれば完了です。

Node 18 以上で SSR 時に $fetch が mock されない

Nuxt の実行環境が Node 18 以上の場合、 そのまま使うと SSR の $fetch (useFetch 含む) を msw がモックしてくれません。

前提

Node18 未満のバージョンではブラウザの fetch 相当の API が標準では実装されていなかったため、 代わりに http/https API を使って HTTP リクエストをする必要がありました。
しかし、node 18 以降から標準で fetch が使えるようになっています。
Node 18 未満でもブラウザの fetch と同等の API が使えるように http/https で実装されたものが node-fetch になります。

$fetch

$fetch とは SSR/CSR を気にせず HTTP リクエストを行うためのヘルパーメソッドです。
内部的には ofetch を使って SSR の時は node-fetch-native が使われるようになっています。
さらに node-fetch-native は node が標準の fetch を使える場合(Node 18 以降) は、標準の fetch を使い、 標準 fetch が使えない場合は node-fetch を使うようになっています。

useFetch も 単純に useAsyncData と $fetch を使ったラッパーコンポーザブルとなっています。

msw インターセプター

msw では @mswjs/interceptors によって以下のリクエストをインターセプトします。

http.get/http.request
https.get/https.request
XMLHttpRequest
window.fetch

上記の API を利用したサードパーティライブラリであればインターセプトできます。

しかし、$fetch の項で書いたとおり、 Node 18 で Nuxt を実行すると SSR では Node の標準の fetch を使うため、msw はインターセプトすることができず mock ができないことになります。

解決策

現状 msw 側が対応してくれるのを待つ以外は

  • NODE_OPTIONS='--no-experimental-fetch' オプションを使用する
    NODE_OPTIONS='--no-experimental-fetch' yarn dev
    
    ※ 9pid さんにコメントいただきました。ありがとうございます :)
  • Node 18 未満のバージョンを使う
  • http/https を使うライブラリを使う(axios など)

となります。

あとがき

SSR でうまく $fetch が動かず、かなり無駄な時間を消費してしまいましたが、
テスト時の mock を極力排除して結合テストをすることでより多くの信頼性が担保できるため、
うまく活用して行きたいと思います。

GitHubで編集を提案

Discussion

9pid9pid

はじめまして!
記事にまとめていたきありがとうございます^^

Node 18 以上だと SSR 時に mock されない件ですが、
私は NODE_OPTIONS='--no-experimental-fetch' オプションを指定して実行することで、
SSRでも mock が走るように設定することができました。

最適解ではないかもしれませんが、参考になると幸いですmm

はるはる

とても参考になるコメントありがとうございます!
確認して追記させていただきます!

Tashiro YutakaTashiro Yutaka

初めまして、コメント失礼いたします。
plugins/msw.client.tsworker.start は Service Worker の有効化が完了したときに Promise を解決するので await で待機した方が良いと思いました。

(同じようなコードを実行していて Service Worker の有効化を待機できていないので、 useFetch によるリクエストでエラーが発生してしまうことがありました。)

はるはる

コメント気づかず遅くなりすみません!
ご指摘ありがとうございます!修正いたしました!