thoov / mock-socket

Javascript mocking library for WebSockets and Socket.IO
MIT License
797 stars 118 forks source link

Unable to use in successive Cypress tests #288

Open tuzmusic opened 4 years ago

tuzmusic commented 4 years ago

Problem

I am trying to use mock-sockets with Cypress, setting up the mock in the onBeforeLoad hook for cy.visit() in my beforeEach block. I can get one test to work but when the mock setup runs on the next beforeEach I get an error that A mock server is already listening on this url.

code under test:

(called from my React app's componentDidiMount)

  subscribeToSettings(url: string): W3CWebSocket {
    let settingsSubscription = new W3CWebSocket(url);
    settingsSubscription.onopen = () => console.log('WebSocket Client Connected (settings)');
    settingsSubscription.onclose = () => console.log('WebSocket Client Disconnected (settings)');
    settingsSubscription.onmessage = (message: MessageEvent) => this.handleSettingsMessage(message);
    return settingsSubscription;
  }

  /**
   * Handler for websocket settings messages, which updates the local settings values.
   * @param message the websocket message
   */
  handleSettingsMessage(message: MessageEvent) {
    const updatedValues = JSON.parse(message.data);
    console.log('A message was received on the settings channel.', updatedValues);
    this.props.updateSettingsFromBackend(updatedValues);
  }

cypress tests

import { Server } from 'mock-socket'
import { defaultSettingsState } from "../../src/reducers/settings.reducer";
import { _createSettingsApiPutPayload } from "../../src/actions/settings.actions";

describe('mock socket method 1', () => {
  let mockSocket;
  let mockServer;
  beforeEach(() => {
    cy.visit('/', {
      onBeforeLoad(win: Window): void {
        // @ts-ignore
        cy.stub(win, 'WebSocket', url => {
          mockServer = new Server(url)
          mockServer.on('connection', socket => {
            console.log('mock socket connected');
            mockSocket = socket;
          });
          mockSocket = new WebSocket(url);
          return mockSocket
        });
      },
    });
  });

  afterEach(() => {
    mockSocket.close()
    mockServer.stop()
  });

  it('gets a message', () => {
    cy.contains('SETTINGS').click()
    const object = _createSettingsApiPutPayload(defaultSettingsState)
    mockSocket.send(JSON.stringify(object));
    cy.contains('Motion threshold')
  });
  it('gets another message', () => {
    cy.contains('SETTINGS').click()
    const object = _createSettingsApiPutPayload(defaultSettingsState)
    mockSocket.send(JSON.stringify(object));
    cy.contains('Motion threshold')
  });
});

Here are the logs from my console:

WebSocket Client Connected (settings)
mock socket connected at url ws://localhost:8702/PM_Settings
A message was received on the settings channel. {…}
mock socket connected at url ws://localhost:3000/sockjs-node/949/mhuyekl3/websocket
The development server has disconnected.
Refresh the page if necessary.
Uncaught Error: A mock server is already listening on this url

I wonder if it has to do with that second call which is for some mystery url.

(Note: calling cy.contains('SETTINGS').click() at the end of beforeEach somehow doesn't work, even in that first test. Even when I have my app set to start on the settings page (instead of having to click to it from inside the tests), clicking on SETTINGS from beforeEach still doesn't work even though we're already there. So that's kind of weird)

These full cypress logs may also be helpful: image

jrfornes commented 2 years ago

I fixed the A mock server is already listening on this url issue with the following implementation:

// websocket-server-mock.ts
import { Server } from "mock-socket";

let mockServer = null;

export class WebsocketServerMock {
  constructor(url: string){
    if (mockServer) {
      mockServer.stop();
      mockServer.close();
      mockServer = null;
    }

    mockServer = new Server(url);
  }

  connect(callback){
    mockServer.on("connection", (socket: any) => {
      if ("function" === typeof callback) {
        callback(socket);
      }
    })
  }
}

And this is how I used this on a test.

import { WebSocket } from "mock-socket";
import { WebsocketServerMock } from "../../support/websocket-server-mock";

beforeEach(() => {
    cy.visit("/#/alarmcentral/global", {
        onBeforeLoad: (win) => {
          cy.stub(win, "WebSocket", (url) => new WebSocket(url));
        },
    });
})

...

it("should test something", () => {
    const { hostname, port } = window.location;
    const url = `ws://${hostname}:${port}/nxt-ui/app/websocket/spectralAnalysis`;
    const mockServer = new WebsocketServerMock(url);
    mockServer.connect((socket) => {
      socket.on("message", console.log);
      socket.send(`{"message":"Successfully connected!"}`);
    });
}
Atrue commented 2 years ago

@tuzmusic According to the screen you have 2 WebSockets called for the first test: /PM_Settings and /sockjs-node/..., so it's why only the latest ws is assigned to mockServer and is cleared in the afterEach hook. Looks like the second ws is related to the hot reloading so you don't need to catch it. It's better to create the mock server first to avoid handling everything else in the stub callback:

mockServer = new Server(url)
mockServer.on('connection', socket => {
    console.log('mock socket connected');
    mockSocket = socket;
});
cy.stub(win, 'WebSocket', url => new WebSocket(url));

So here you will have only pre-defined servers and all other WebSockets will be rejected (It stops the hot reloading there but it doesn't affect the tests. But if you want to keep it you have to add some conditions for this URL in the stub callback to create a native WebSocket at this case)

Atrue commented 2 years ago

@jrfornes Looks like your mock server is running across multiple tests until the new instance of WebsocketServerMock is created. It's better to clear all mocks in afterEach hook, so it makes the code clear and avoids weird behavior. (Like If you forgot to create a new instance the previous mock server will catch the next WebSocket)