とろろこんぶろぐ

かけだしR&Dフロントエンジニアの小言

msw が Service Worker に依存する時代が終わっていた話

概要

msw はいつの間にかモックツールとしてデファクトスタンダードになりました。

github.com

Mock Service Worker という名の通り、Service Worker を利用して、アプリが API サーバーとやりとりするリクエスト/レスポンスをモックすることができるツールです。

ただ、ふと気がついたら Service Worker なしでモックできるようになっていたので、その小ネタを書きます。

この記事は Recruit Engineers Advent Calendar 2022 の9日目の記事です。

adventar.org

Node でも使える msw

ご存じの方も多いと思いますが、 msw は Node.js でも利用できます。

Node - Getting Started - Mock Service Worker Docs

Node.js で利用できるのはわかりましたが、 Service Worker は Node.js では動作しません

では、どうやって Node.js で msw を動かしているかというと、 これは Node.js の httpintercept してモック処理を実現しています。

msw 内で使われているモジュールはこれ↓

github.com

Node 向けの msw 起動 API setupServer を呼び出すと、内部で SetupServerApi クラスを作ります。

https://github.com/mswjs/msw/blob/main/src/node/setupServer.ts#L15

そこで上記で紹介したモジュールの interceptors のうち、ClientRequestInterceptor が登録されます。

https://github.com/mswjs/msw/blob/main/src/node/SetupServerApi.ts#L46-L49

これで Node.js の http から発行されるリクエストを横取りして加工することができるようになります。

具体的な処理はおおよそ以下のようになっています。

this.interceptor.on('request', async (request) => {
      // モック用のリクエストを生成
      const mockedRequest = new MockedRequest(request.url, {...})

      // msw で登録した handler でレスポンスを生成
      const response = await handleRequest(...)

      // 生成したレスポンスのモックデータを返す
      if (response) {
        ...
        request.respondWith(response)
      }

      return
})

msw の listern を開始したら上記 'request' イベントのコールバックハンドラを登録します。

Node.js の内部で request.end が発火されたタイミングで emitter を発火させて上記が処理されます。

  // 'request' イベントを発火することで intercepter のコールバックが動く
  this.emitter.emit('request', interactiveRequest, requestId)

https://github.com/mswjs/interceptors/blob/main/src/interceptors/ClientRequest/NodeClientRequest.ts#L154

Node.js は http.get を呼び出すと必ず自動的に req.end を呼び出すと Node.js のドキュメントに書かれているので、

HTTP | Node.js v19.3.0 Documentation

request.end を hook しておくことで Node.js の request 処理をモックできるようにしているようです。

これで、Nose.js で Service Worker を使わずとも msw を利用できるようになっています。

ブラウザでの msw

msw はブラウザ側で利用する場合 Service Worker が使われます。

しかし実は Service Worker が利用できないケースでもちゃんと動作してくれます。 Service Worker が利用できないケースというと具体的には、 https 化されていないサーバーで Web ページが提供される場合などです。

Service Worker は https でないと動作させることができません*1

例えば、 Storybook で msw を利用していて、その Storybook をビルドして S3 にアップロードし、社内だけで閲覧できるように共有するとします。 特に何も意識せず S3 を公開すると http://hogehoge-storybook.s3-website-ap-northeast-1.amazonaws.com/ といった http はじまりの URL で共有されます。

これを実際に表示すると、https じゃないので msw は Service Worker が使えず利用できないかと思いきや、なんときちんと動作します。

ブラウザ向けの msw 起動 API setupWorker を呼び出すと、内部で以下のような処理が走ります。

      // service worker が利用できないフラグを立てておき
      useFallbackMode:
        !('serviceWorker' in navigator) || location.protocol === 'file:',

...
    // フラグが立っていたらフォールバックモードで起動する
    this.startHandler = context.useFallbackMode
      ? createFallbackStart(context)
      : createStartHandler(context)

https://github.com/mswjs/msw/blob/main/src/setupWorker/setupWorker.ts#L175-L177

そこで上記で紹介した interceptors のうち、今度は FetchInterceptor が登録されます。

https://github.com/mswjs/msw/blob/main/src/setupWorker/start/createFallbackRequestListener.ts#L25

そうすると Node のときとほぼ同じようなコードがあります。

  interceptor.on('request', async (request) => {
    // モック用のリクエストを生成
    const mockedRequest = new MockedRequest(request.url, { ... })

    // msw で登録した handler でレスポンスを生成
    const response = await handleRequest<SerializedResponse>( ... )

    // 生成したレスポンスのモックデータを返す
    if (response) {
      request.respondWith(response)
    }
  })

FetchInterceptor は名前の通り fetch を intercept できます。

https://github.com/mswjs/interceptors/blob/main/src/interceptors/fetch/index.ts

  protected setup() {
    // 元々の fetch を退避させておき
    const pureFetch = globalThis.fetch

    ...
    // globalThis の fetch を独自に上書きする
    globalThis.fetch = async (input, init) => {

      ...
      // その中で 'request' イベントを発火して↑のハンドラを実行する
      this.emitter.emit('request', interactiveRequest, requestId)

      ...
      // モックが実行されなければ pureFetch を叩く
      return pureFetch(request).then((response) => { ...

https://github.com/mswjs/interceptors/blob/main/src/interceptors/fetch/index.ts#L23-L42

ブラウザ側で実行される globalThis.fetch を加工してしまうことで、 Service Worker を使わずとも msw のモックが実現できます。

つまり、もはや msw は Mock Service Worker と言っておきながら Node, ブラウザともに Service Worker に依存せず動作することができるツールとなっていました。

Node v18 Fetch 対応

上記で一応記事の内容としては終わりですが、一応補足的に Node v18 で入った fetch について記載しておきます。

Node v18 で experimental ですが fetch が利用できるようになりました。

Node.js 18 is now available! | Node.js

先ほど msw が Node.js 上で intercept していたのは、 http でした。 これに加えて fetch も intercept できるのかというと、現状ではまだ対応しきれていないようです。

記事執筆時点で、 msw の Node v18 対応の Issue はオープン状態でした。

github.com

この辺りも似た Issue ですね。

Node 17 and 18 silently don't get intercepted · Issue #246 · mswjs/interceptors · GitHub feat: support Node.js 18 by milesrichardson · Pull Request #283 · mswjs/interceptors · GitHub Support Undici · Issue #159 · mswjs/interceptors · GitHub

まとめ

msw が Service Worker に依存せず動作することができることを紹介しました。

  • Node.js → http のリクエスト/レスポンスを intercept
  • ブラウザ → デフォルトでは Service Worker だが動作しなくても fetch を intercept

msw が多様なユースケースで利用されることで、気付かぬ間にいろいろ進化しているんだなと知ることができました。

*1:http でもlocalhost のみ例外的に許容されています