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 を使ってリアルタイムでメッセージを送受信するアプリケーションです。
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 サーバーのエンドポイントを指定します。
import { ws } from "msw";
const chat = ws.link("ws://localhost:8080");
次に、chat
に対してリクエストハンドラーを設定します。addEventListener
メソッドを使って、connection
イベントを監視しログを出力します。
export const handlers = [
chat.addEventListener("connection", () => {
console.log("A new client connected", "👻");
}),
];
続いて browser.ts
でリクエストハンドラーを登録します。
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
アプリケーションのエントリーポイントで worker.start()
を呼び出してモックサーバーが起動されるようにします。
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
Web Socket のリクエストのモックでは Service Worker を利用しないため、このコマンドはスキップすることも可能です。Service Worker は HTTP や GraphQL などのリクエストをモックする際に利用されます。
これで Web Socket のリクエストをモックする準備が整いました。アプリケーションを起動し、ブラウザでアクセスすると、コンソールに A new client connected 👻
というログが出力されます。
フォームを使ってメッセージを送信すると、メッセージが送信されたことを示す「⬆」とともにログが出力されることを確認できます。
Web Socket のモックは WebSocket
クラスにパッチを適用することで行われているため、HTTP や GraphQL のモックと異なり DevTools の Network タブにはリクエストが表示されません。そのため MSW ではブラウザ内のモックされた WebSocket 接続と元の WebSocket 接続の両方に対してカスタムログを出力しています。
クライアントイベントをモックする
ここまでで Web Socket の接続をモックできることを確認しました。続いてクライアントのイベントをモックして、実際にメッセージの送受信を行っているように見えるように処理を実装します。
クライアントのイベントをモックするためには connection
イベントの引数の client
オブジェクトを使用します。client.addEventListener
メソッドで message
イベントを監視することで、クライアントからのメッセージを受信できます。
クライアントにメッセージを送信する
client.send
メソッドを使って受信したメッセージをそのままクライアントに送信しています。
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
メソッドを使って、クライアントから受信したメッセージを全てのクライアントにブロードキャストできます。
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
メソッドを使って、特定のクライアントにメッセージを送信しないようにすることも可能です。
export const handlers = [
chat.addEventListener("connection", ({ client }) => {
client.addEventListener("message", (event) => {
// 送信元のクライアントにはメッセージを送信しない
client.broadcastExcept(client, event.data);
});
}),
];
接続を閉じる
client.close
メソッドを使ってクライアントとの接続を閉じることができます。例として「/close
」というメッセージを受信した場合に接続を閉じるようにしてみましょう。close()
の 1 番目の引数にはクローズコード、2 番目の引数にはクローズした理由を指定できます。
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
イベントを監視して、接続が閉じられたときの処理を記述します。
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 サーバーが起動している必要があります。
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()
メソッドを使用してデータを変更したからサーバーにメッセージを送信できます。
export const handlers = [
chat.addEventListener("connection", ({ client, server }) => {
server.connect();
client.addEventListener("message", (event) => {
// サーバーにメッセージが送信されることを防ぐ
event.preventDefault();
// データを変更してからサーバーにメッセージを送信
server.send(event.data + "mocked");
// ...
});
}),
];
実際のサーバーからのメッセージを受信するためには message
イベントを監視します。
export const handlers = [
chat.addEventListener("connection", ({ client, server }) => {
server.connect();
// ...
server.addEventListener("message", (event) => {
console.log("Received message from server 👻", event.data);
});
}),
];
デフォルトではすべてのサーバーからのメッセージはクライアントに転送されます。これを防ぐためには event.preventDefault()
を呼び出します。
export const handlers = [
chat.addEventListener("connection", ({ client, server }) => {
server.connect();
// ...
server.addEventListener("message", (event) => {
// クライアントにメッセージが送信されることを防ぐ
event.preventDefault();
// メッセージを変更してからクライアントに送信
client.send(event.data + "mocked");
});
}),
];
サーバーへの接続を閉じるためには server.close()
メソッドを呼び出します。
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 を持つハンドラーを作成できます。
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 を持つハンドラーを作成できる