A set of utilities and matchers to aid in mocking websocket servers in vitest.
Built on top of mock-socket and a refactored implementation of vitest-websocket-mock and jest-websocket-mock
npm install -D vitest-mock-socketImport and instantiate the instance
import { WebSocketServer } from 'vitest-mock-socket';
const server = new WebSocketServer(url);Connect a client to the same url
const client = new WebSocket(url);Wait for the server to connect
await server.connected(); The server will record all messages it receives
client.send('hello');The server can also send messages to all connected clients
server.send('hello everyone');The server will also handle json messages out of the box
server.send({ foo: 'bar' })Simulate an error and close the connection
server.error();Gracefully close the connection
server.close();The instance also has a static method to gracefully close all open connections. This is particularly useful to reset the environment between test runs.
WebSocketServer.clean();connected- a Promise that resolves every time the mock server receives a new connection. The resolved value is the WebSocket client instance that initiated the connection.
closed- a Promise that resolves every time a connection to the mock server is closed.
nextMessage- a Promise that resolves every time a mock server instance receives a new message.
send- send a message to all connected clients.
close- gracefully closes all opened connections.
error- sends an error message to all connected clients and closes all opened connections.
on- attach event listeners to handle new
connection,messageandcloseevents. The callback receives thesocketas its only argument.
- attach event listeners to handle new
The constructor accepts an optional options object as second argument.
The options supported by the mock-socket library are directly passed-through to the mock-server's constructor.
export interface WebSocketServerOptions extends MockSocket.ServerOptions {}const server = new WebSocketServer(url, options);A verifyClient function can be given in the options for the vitest-mock-socket constructor.
This can be used to test behavior for a client that connects to a WebSocket server that it is blacklisted from. For example:
Note : Currently mock-socket's implementation does not send any parameters to this function (unlike the real ws implementation).
test('rejects connections that fail the verifyClient option', async () => {
new WebSocketServer('ws://localhost:1234', { verifyClient: () => false });
const errorCallback = vitest.fn();
await expect(
new Promise((resolve, reject) => {
errorCallback.mockImplementation(reject);
const client = new WebSocket(url);
client.onerror = errorCallback;
client.onopen = resolve;
})
// WebSocket onerror event gets called with an event of type error and not an error
).rejects.toEqual(expect.objectContaining({ type: 'error' }));
});A selectProtocol function can be given in the options for the vitest-mock-socket constructor.
This can be used to test behaviour for a client that connects to a WebSocket server using the wrong protocol.
test('rejects connections that fail the selectProtocol option', async () => {
const selectProtocol = () => null;
new WebSocketServer('ws://localhost:1234', { selectProtocol });
const errorCallback = vitest.fn();
await expect(
new Promise((resolve, reject) => {
errorCallback.mockImplementationOnce(reject);
const client = new WebSocket('ws://localhost:1234', 'foo');
client.onerror = errorCallback;
client.onopen = resolve;
})
).rejects.toEqual(
// WebSocket onerror event gets called with an event of type error and not an error
expect.objectContaining({
type: 'error',
currentTarget: expect.objectContaining({ protocol: 'foo' }),
})
);
});Custom vitest matchers are included to ease running assertions on received messages:
An async matcher that waits for the next message received by the the mocked websocket server, and asserts its content. It will time out with a helpful message after 1000ms.
test('the server keeps track of received messages, and yields them as they come in', async () => {
const server = new WebSocketServer(url);
const client = new WebSocket(url);
await server.connected();
client.send('hello');
await expect(server).toReceiveMessage('hello');
});A synchronous matcher that checks that all the expected messages have been received by the mock websocket server.
Note: Since this matcher is synchronous, there are situations where you must call await server.nextMessage() for each message sent before asserting with this matcher. You can get the same behavior without the need to manually call .nextMessage by using the asynchronous variant toHaveResolvedMessages
test('the server keeps track of received messages, and yields them as they come in', async () => {
const server = new WebSocketServer(url);
const client = new WebSocket(url);
await server.connected();
client.send('hello');
client.send('goodbye');
await server.nextMessage();
await server.nextMessage();
expect(server).toHaveReceivedMessages(['hello', 'goodbye']);
});test('server handles mixed message types', async () => {
const server = new WebSocketServer(url);
const client = new WebSocket(url);
await server.connected();
client.send('hello there');
client.send(`{"type":"GREETING","payload":"how are you?"}`);
client.send(`{"type":"GREETING","payload":"good?"}`);
await server.nextMessage();
await server.nextMessage();
await server.nextMessage();
expect(server).toHaveReceivedMessages([
'hello there',
{ type: 'GREETING', payload: 'how are you?' },
{ type: 'GREETING', payload: 'good?' },
]);
});A asynchronous version of toHaveReceivedMessages. It will automatically resolve the message queue so you do not have to manually call server.nextMessage.
Default behavior is to match exactly.
test('the server keeps track of received messages, and yields them as they come in', async () => {
const server = new WebSocketServer(url);
const client = new WebSocket(url);
await server.connected();
client.send('hello');
client.send('goodbye');
await expect(server).toHaveResolvedMessages(['hello', 'goodbye']);
});This would fail
test('the server keeps track of received messages, and yields them as they come in.', async () => {
const server = new WebSocketServer(url);
const client = new WebSocket(url);
await server.connected();
client.send('hello');
client.send('goodbye');
await expect(server).toHaveResolvedMessages(['hello' ]);
});Accepts an options object to allow for partial matches.
test('the server keeps track of received messages, and yields them as they come in.', async () => {
const server = new WebSocketServer(url);
const client = new WebSocket(url);
await server.connected();
client.send('hello');
client.send('goodbye');
await expect(server).toHaveResolvedMessages(['hello'], { partial: true });
}); test('server handles mixed message types', async () => {
const server = new WebSocketServer(url);
const client = new WebSocket(url);
await server.connected();
client.send('hello there');
client.send(`{"type":"GREETING","payload":"how are you?"}`);
client.send(`{"type":"GREETING","payload":"good?"}`);
await expect(server).toHaveResolvedMessages([
'hello there',
{ type: 'GREETING', payload: 'how are you?' },
{ type: 'GREETING', payload: 'good?' },
]);
});test('the mock server sends messages to connected clients', async () => {
const server = new WebSocketServer(url);
const client1 = new WebSocket(url);
await server.connected();
const client2 = new WebSocket(url);
await server.connected();
const messages = { client1: [], client2: [] };
client1.onmessage = (e) => {
messages.client1.push(e.data);
};
client2.onmessage = (e) => {
messages.client2.push(e.data);
};
server.send('hello everyone');
expect(messages).toEqual({
client1: ['hello everyone'],
client2: ['hello everyone'],
});
});test('the mock server sends errors to connected clients', async () => {
const server = new WebSocketServer(url);
const client = new WebSocket(url);
await server.connected();
let disconnected = false;
let error = null;
client.onclose = () => {
disconnected = true;
};
client.onerror = (e) => {
error = e;
};
server.send('hello everyone');
server.error();
expect(disconnected).toBe(true);
expect(error.origin).toBe('ws://localhost:1234/');
expect(error.type).toBe('error');
});it('the server can refuse connections', async () => {
const server = new WebSocketServer(url);
server.on('connection', (socket) => {
socket.close({ wasClean: false, code: 1003, reason: 'NOPE' });
});
const client = new WebSocket(url);
client.onclose = (event: CloseEvent) => {
expect(event.code).toBe(1003);
expect(event.wasClean).toBe(false);
expect(event.reason).toBe('NOPE');
};
expect(client.readyState).toBe(WebSocket.CONNECTING);
await server.connected();
expect(client.readyState).toBe(WebSocket.CLOSING);
await server.closed();
expect(client.readyState).toBe(WebSocket.CLOSED);
});You can set up a mock server and a client, and reset them between tests:
beforeEach(async () => {
server = new WebSocketServer(url);
client = new WebSocket(url);
await server.connected();
});
afterEach(() => {
WebSocketServer.clean();
});mock-socket has a strong usage of delays (setTimeout to be more specific). This means using vi.useFakeTimers(); will cause issues such as the client appearing to never connect to the server.
While running the websocket server from tests within the vitest-dom environment (as opposed to node) you may see errors of the nature:
ReferenceError: setImmediate is not definedYou can work around this by installing the setImmediate shim from
https://github.com/YuzuJS/setImmediate and
adding require('setimmediate'); to your setupTests.js.
vitest-mock-socket uses the mock-socket library.
under the hood to mock out WebSocket clients.
Out of the box, mock-socket will only mock out the global WebSocket object. If you are using a third-party WebSocket client library (eg. a Node.js
implementation, like ws), you'll need to set up a manual mock:
- Create a
__mocks__folder in your project root - Add a new file in the
__mocks__folder named after the library you want to mock out. For instance, for thewslibrary:__mocks__/ws.js. - Export Mock Socket's implementation in-lieu of the normal export from the
library you want to mock out. For instance, for the
wslibrary:
// __mocks__/ws.js
export { WebSocket as default } from 'mock-socket';- Somewhere in the test files, call
vi.mockwith the name of the library you want to mock. For instance, for the ws library:
// example.test.js
import WebSocket from 'ws';
import { vi } from 'vitest';
vi.mock('ws');
// do some tests...NOTE The ws library is not 100% compatible with the browser API, and vitest-mock-socket's dependency mock-socket only implements the browser API.
As a result, vitest-mock-socket will only work with the ws library if you restrict yourself to the browser APIs.