Skip to content

andrewangelle/vitest-mock-socket

Repository files navigation

vitest mock socket

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 version
Build Status

Install

npm install -D vitest-mock-socket

Usage

Import 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();

WebSocketServer constructor

Methods

  • 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, message and close events. The callback receives the socket as its only argument.

Options

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);
verifyClient

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' }));
});
selectProtocol

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' }),
    })
  );
});

Vitest matchers

Custom vitest matchers are included to ease running assertions on received messages:

.toReceiveMessage

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');  
});

.toHaveReceivedMessages

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?' },
  ]);
});

.toHaveResolvedMessages

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?' },
    ]);
  });

Other examples

Send messages to multiple connected clients

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'],
  });
});

Sending errors

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');
});

Refuse connections example:

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);
});

Environment set up and tear down between tests

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();
});

Known issues

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 defined

You can work around this by installing the setImmediate shim from https://github.com/YuzuJS/setImmediate and adding require('setimmediate'); to your setupTests.js.

Using vitest-mock-socket to interact with a non-global WebSocket object

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 the ws library: __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 ws library:
// __mocks__/ws.js

export { WebSocket as default } from 'mock-socket';
  • Somewhere in the test files, call vi.mock with 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.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •