あんみつのイラスト

MSW で Web Socket のリクエストをモックする

Mock Service Worker (MSW) の v2.6.0 から Web Socket のリクエストをモックすることができるようになりました。Web Socket のサポートのリクエストは 2020 年から存在しており、多くの議論の末 4 年の歳月を経てリリースされた機能となります。この記事では、MSW で Web Socket のリクエストをモックする方法を紹介します。

Mock Service Worker (MSW) の v2.6.0 から Web Socket のリクエストをモックできるようになりました。Web Socket のサポートのリクエストは 2020 年から存在しており、多くの議論の末 4 年の歳月を経てリリースされた機能となります。

この記事では、MSW で Web Socket のリクエストをモックする方法を紹介します。

Web Socket のリクエストをモックする

まずは Web Socket を使ったアプリケーションを作成しましょう。以下のコードは Web Socket を使ってリアルタイムでメッセージを送受信するアプリケーションです。

Chat.tsx
import React, { useState, useEffect, useRef } from "react";
 
export const Chat: React.FC = () => {
  const ws = useRef<WebSocket | null>(null);
  const [messages, setMessages] = useState<string[]>([]);
  const [message, setMessage] = useState("");
 
  useEffect(() => {
    // websocket サーバーの接続を確立する
    ws.current = new WebSocket("ws://localhost:8080");
 
    // メッセージを受信したときの処理
    ws.current.onmessage = (event) => {
      setMessages((prevMessages) => [...prevMessages, event.data]);
    };
 
    return () => {
      // コンポーネントがアンマウントされたときに websocket サーバーとの接続を閉じる
      ws.current?.close();
    };
  }, []);
 
  const sendMessage = () => {
    if (!ws.current) {
      return;
    }
    // フォームがサブミットされたときにメッセージを送信する
    ws.current.send(message);
    setMessage("");
  };
 
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        sendMessage();
      }}
    >
      <ul>
        {messages.map((message, index) => (
          <li key={index}>{message}</li>
        ))}
      </ul>
      <input
        type="text"
        value={message}
        onChange={(event) => setMessage(event.target.value)}
      />
      <button>Send</button>
    </form>
  );
};

簡単にコードを説明します。useEffect フックでコンポーネントがマウントされたときに WebSocket インスタンスを作成し、サーバーへの接続を確立します。

ws.onmessage はサーバーからメッセージを受信したときに呼び出されるコールバック関数です。受信したメッセージは messages 配列に追加され、UI 上のリストに表示されます。

フォームがサブミットされた場合には sendMessage 関数が呼び出され、ws.send メソッドによりサーバーへメッセージが送信されます。ここで送信されたメッセージはサーバーから全てのクライアントにブロードキャストされ、ws.onmessage で受信されます。

このチャットアプリケーションを動かせるようにするために、MSW を使って Web Socket のリクエストをモックしましょう。まずは MSW をインストールします。

npm install msw

続いて、MSW のリクエストハンドラーを作成します。はじめに ws.link() 関数を使って MSW がモックする Web Socket サーバーのエンドポイントを指定します。

handlers.ts
import { ws } from "msw";
 
const chat = ws.link("ws://localhost:8080");

次に、chat に対してリクエストハンドラーを設定します。addEventListener メソッドを使って、connection イベントを監視しログを出力します。

handlers.ts
export const handlers = [
  chat.addEventListener("connection", () => {
    console.log("A new client connected", "👻");
  }),
];

続いて browser.ts でリクエストハンドラーを登録します。

browser.ts
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
 
export const worker = setupWorker(...handlers);

アプリケーションのエントリーポイントで worker.start() を呼び出してモックサーバーが起動されるようにします。

main.tsx
import { worker } from './browser.ts'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Chat } from './Chat'
 
// アプリケーションで実運用する場合には、開発環境のみで `worker.start()` が呼び出されるようにする
worker.start()
 
createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <Chat />
  </StrictMode>,
)

最後に msw init コマンドを実行して MSW が利用する Service Worker を登録します。

npx msw init public

Note

Web Socket のリクエストのモックでは Service Worker を利用しないため、このコマンドはスキップすることも可能です。Service Worker は HTTP や GraphQL などのリクエストをモックする際に利用されます。

これで Web Socket のリクエストをモックする準備が整いました。アプリケーションを起動し、ブラウザでアクセスすると、コンソールに A new client connected 👻 というログが出力されます。

フォームを使ってメッセージを送信すると、メッセージが送信されたことを示す「⬆」とともにログが出力されることを確認できます。

Tip

Web Socket のモックは WebSocket クラスにパッチを適用することで行われているため、HTTP や GraphQL のモックと異なり DevTools の Network タブにはリクエストが表示されません。そのため MSW ではブラウザ内のモックされた WebSocket 接続と元の WebSocket 接続の両方に対してカスタムログを出力しています。

クライアントイベントをモックする

ここまでで Web Socket の接続をモックできることを確認しました。続いてクライアントのイベントをモックして、実際にメッセージの送受信を行っているように見えるように処理を実装します。

クライアントのイベントをモックするためには connection イベントの引数の client オブジェクトを使用します。client.addEventListener メソッドで message イベントを監視することで、クライアントからのメッセージを受信できます。

クライアントにメッセージを送信する

client.send メソッドを使って受信したメッセージをそのままクライアントに送信しています。

handlers.ts
import { ws } from "msw";
 
const chat = ws.link("ws://localhost:8080");
 
export const handlers = [
  chat.addEventListener("connection", ({ client }) => {
    client.addEventListener("message", (event) => {
      console.log("Received message 👻", event.data);
      client.send(event.data);
    });
  }),
];

実際にアプリケーションでメッセージを送信してみると、送信したメッセージがそのまま受信されることを確認できます。モックサーバーから受信したメッセージは「⬇」とともにログに出力されます。

メッセージをブロードキャストする

chat.broadcast メソッドを使って、クライアントから受信したメッセージを全てのクライアントにブロードキャストできます。

handlers.ts
import { ws } from "msw";
 
const chat = ws.link("ws://localhost:8080");
 
export const handlers = [
  chat.addEventListener("connection", ({ client }) => {
    client.addEventListener("message", (event) => {
      client.broadcast(event.data);
    });
  }),
];

これにより別のクライアントが送信したメッセージも受信できます。

.broadcastExcept メソッドを使って、特定のクライアントにメッセージを送信しないようにすることも可能です。

handlers.ts
export const handlers = [
  chat.addEventListener("connection", ({ client }) => {
    client.addEventListener("message", (event) => {
      // 送信元のクライアントにはメッセージを送信しない
      client.broadcastExcept(client, event.data);
    });
  }),
];

接続を閉じる

client.close メソッドを使ってクライアントとの接続を閉じることができます。例として「/close」というメッセージを受信した場合に接続を閉じるようにしてみましょう。close() の 1 番目の引数にはクローズコード、2 番目の引数にはクローズした理由を指定できます。

handlers.ts
import { ws } from "msw";
 
const chat = ws.link("ws://localhost:8080");
 
export const handlers = [
  chat.addEventListener("connection", ({ client }) => {
    client.addEventListener("message", (event) => {
      if (event.data === "/close")  {
        client.close(1000, "client request");
        return
      }
      chat.broadcast(event.data);
    });
  }),
];

クライアント側のコードを修正して、クライアント側の要求により接続が閉じられた場合にメッセージが閉じられたことを示すメッセージを表示するようにしましょう。ws.onclose イベントを監視して、接続が閉じられたときの処理を記述します。

Chat.tsx
import React, { useState, useEffect, useRef } from "react";
 
export const Chat: React.FC = () => {
  const ws = useRef<WebSocket | null>(null);
  const [messages, setMessages] = useState<string[]>([]);
  const [message, setMessage] = useState("");
  const [isClosed, setIsClosed] = useState(false);
 
  useEffect(() => {
    // websocket サーバーの接続を確立する
    ws.current = new WebSocket("ws://localhost:8080");
 
    // ...
 
    // 接続が閉じられたときの処理
    ws.current.onclose = (e) => {
      // クライアントの要求により接続が閉じられた場合
      if (e.code === 1000 && e.reason === "client request") {
        setIsClosed(true);
      }
      // その他の理由で接続が閉じられた場合再接続を試みるべきだがここでは省略
    };
 
    // ...
  }, []);
 
  // ...
 
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        sendMessage();
      }}
    >
      {isClosed && <p style={{ color: "red" }}>Connection closed</p>}
      <ul>
        {messages.map((message, index) => (
          <li key={index}>{message}</li>
        ))}
      </ul>
      <input
        type="text"
        value={message}
        onChange={(event) => setMessage(event.target.value)}
      />
      <button>Send</button>
    </form>
  );
};

実際に /close というメッセージを送信すると、クライアント側に「Connection closed」というメッセージが表示されることを確認できます。その後は接続が閉じられているため、メッセージの送受信ができなくなります。

サーバーイベントを確立する

connection イベントのコールバック引数の server オブジェクトを使うと、ws.link() で指定したエンドポイントに対して接続を確立できます。sever.connect() メソッドを呼び出す場合には、本物の Web Socket サーバーが起動している必要があります。

handlers.ts
import { ws } from "msw";
 
const chat = ws.link("ws://localhost:8080");
 
export const handlers = [
  chat.addEventListener("connection", ({ client, server }) => {
 
    server.connect();
 
    // ...
  }),
];

サーバーへ接続が確立されている場合には、すべてのクライアントの送信メッセージがサーバーに送信されます。この動作を防ぐ場合には event.preventDefault() を呼び出すことで、クライアントからのメッセージをブロックできます。

その後 server.send() メソッドを使用してデータを変更したからサーバーにメッセージを送信できます。

handlers.ts
export const handlers = [
  chat.addEventListener("connection", ({ client, server }) => {
    server.connect();
 
    client.addEventListener("message", (event) => {
      // サーバーにメッセージが送信されることを防ぐ
      event.preventDefault();
 
      // データを変更してからサーバーにメッセージを送信
      server.send(event.data + "mocked");
 
      // ...
    });
  }),
];

実際のサーバーからのメッセージを受信するためには message イベントを監視します。

handlers.ts
export const handlers = [
  chat.addEventListener("connection", ({ client, server }) => {
    server.connect();
 
    // ...
 
    server.addEventListener("message", (event) => {
      console.log("Received message from server 👻", event.data);
    });
  }),
];

デフォルトではすべてのサーバーからのメッセージはクライアントに転送されます。これを防ぐためには event.preventDefault() を呼び出します。

handlers.ts
export const handlers = [
  chat.addEventListener("connection", ({ client, server }) => {
    server.connect();
 
    // ...
 
    server.addEventListener("message", (event) => {
      // クライアントにメッセージが送信されることを防ぐ
      event.preventDefault();
 
      // メッセージを変更してからクライアントに送信
      client.send(event.data + "mocked");
    });
  }),
];

サーバーへの接続を閉じるためには server.close() メソッドを呼び出します。

handlers.ts
export const handlers = [
  chat.addEventListener("connection", ({ client, server }) => {
    server.connect();
 
    // ...
 
    client.addEventListener("message", (event) => {
      if (event.data === "/close") {
        client.close(1000, "client request");
        server.close();
        return;
      }
      server.send(event.data + "mocked");
    });
  }),
];

socket.io バインディング

ハンドラーのコードは標準の WebSocket インターフェースを使って実装されています。ですが、実際の Web Socket サーバーの開発では socket.io などのライブラリを使用して抽象化されたインターフェイスを使っていることも多いでしょう。

このような場合にバインディングを使用できます。バインディングを使用すると生の WebSocket インターフェースを使っているハンドラーラップしてサードパーティライブラリと同じインターフェイスを使用してモックを作成できます。

@mswjs/socket.io-binding パッケージは socket.io のバインディングを提供します。まずはパッケージをインストールします。

npm install @mswjs/socket.io-binding -D

toSocketIo 関数を呼び出すことで、socket.io と同じ API を持つハンドラーを作成できます。

handlers.ts
import { ws } from "msw";
import { toSocketIo } from "@mswjs/socket.io-binding";
 
const chat = ws.link("ws://localhost:8080");
 
export const handlers = [
  chat.addEventListener("connection", (connection) => {
    const io = toSocketIo(connection.client);
 
    io.client.on("message", (message) => {
      io.client.emit("message", message);
    });
  }),
];

まとめ

  • MSW v2.6.0 から Web Socket のリクエストをモックできるようになった
  • Web Socket のリクエストをモックするには ws.link() 関数を使ってエンドポイントを指定し、リクエストハンドラーを設定する
  • connection イベントを監視することで Web Socket の接続をモックできる
  • client オブジェクトを使ってクライアントのイベントをモックし、メッセージの送受信を行う
  • server オブジェクトを使って実際の Web Socket サーバーとの接続を確立し、サーバーからのメッセージを送受信できる
  • @mswjs/socket.io-binding パッケージを使うことで socket.io と同じ API を持つハンドラーを作成できる

参考

記事の理解度チェック

以下の問題に答えて、記事の理解を深めましょう。

Web Socket のモックを行うために、クライアントが接続するエンドポイントを指定する関数は何か?

  • ws.connect()

    もう一度考えてみましょう

  • ws.link()

    正解!

  • ws.on()

    もう一度考えてみましょう

  • ws.url()

    もう一度考えてみましょう

Web Socket のクローズコード、クローズ理由を指定して接続を閉じる方法として正しいものはどれか?

  • client.close(1000, 'client request')

    正解!

  • client.close({ code: 1000, reason: 'client request' })

    もう一度考えてみましょう

  • client.close('client request', 1000)

    もう一度考えてみましょう

  • client.close(1000, { reason: 'client request' })

    もう一度考えてみましょう


Contributors

> GitHub で修正を提案する
この記事をシェアする
はてなブックマークに追加

関連記事