Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mock websockets #2492

Open
Tracked by #1
maxime1992 opened this issue Sep 19, 2018 · 38 comments
Open
Tracked by #1

Mock websockets #2492

maxime1992 opened this issue Sep 19, 2018 · 38 comments
Labels
E2E Issue related to end-to-end testing topic: cy.intercept() type: feature New feature that does not currently exist

Comments

@maxime1992
Copy link

maxime1992 commented Sep 19, 2018

Current behavior:

We can use cy.server() and cy.route(...) to mock HTTP calls.

Desired behavior:

The exact same thing for Websockets.

For apps using websockets quite heavily this would come really handy.

Might be related to #199

Would be great to have the ability to watch a bi-directional web socket to process messages from the back-end as they are received.

Use Case:

User preforms a action through the front-end which actions several back-end events. It would be great if we could monitor that these different events did occur (there is an endless amount of different events and combinations of events).

What would be great if there was a simple message pool/queue or a way to "monitor" a bi-direcitonal websocket and capturing the messages received from the back-end.

We would then write tests to check if those events are being executed by looking at the socket/pool/queue and seeing if the events were received after X user actions.

@jennifer-shehane jennifer-shehane added the stage: proposal 💡 No work has been done of this issue label Sep 20, 2018
@jennifer-shehane jennifer-shehane added type: feature New feature that does not currently exist difficulty: 4️⃣ labels Jan 28, 2019
@MosheAtOwal
Copy link

Related: sinonjs/sinon#174

@EmmanuelDemey
Copy link

Hi

Any news about this feature ?

Thanks

@quentinus95
Copy link

quentinus95 commented Aug 19, 2019

@EmmanuelDemey you can already mock WebSockets in Cypress (as a workaround) the same way you do it for fetch, using onBeforeLoad in visit. I use the mock-socket library:

onBeforeLoad(win) {
    // Call some code to initialize the fake server part using MockSocket
    cy.stub(win, "WebSocket", url => new MockSocket.WebSocket(url))
}

@Tav0
Copy link

Tav0 commented Aug 23, 2019

@EmmanuelDemey you can already mock WebSockets in Cypress (as a workaround) the same way you do it for fetch, using onBeforeLoad in visit. I use the mock-socket library:

onBeforeLoad(win) {
    // Call some code to initialize the fake server part using MockSocket
    cy.stub(win, "WebSocket", url => new MockSocket.WebSocket(url))
}

How did you solve the issue with sockjs-node?

@quentinus95
Copy link

Which issue? You just need the mock-socket library to get it work.

@theAndrewCline
Copy link

theAndrewCline commented Aug 27, 2019

@quentinus95

I would be interested in seeing a full example of your implementation of the MockSocket Library. I'm struggling to get it to work correctly. (partly because I was trying to use the RxJS WS Subject)

In your example, you are passing the URL from the window object it looks like is that right?

@EmmanuelDemey
Copy link

Yes. a full example should be very helpful.

@theAndrewCline
Copy link

I have an example that is almost working the way I want it to now. Having an issue with the Server I believe. for some reason, it will not close and tries to reconnect and cypress keeps picking up xhr requests so it doesn't close.

However, I was able to stub out the client web socket correctly.

cy.visit('/', {
      onBeforeLoad: (win) => {
        cy.stub(win, 'WebSocket', (url) => {
          MockServer = new Server(url)
          MockServer.on('connection', (socket) => {
            socket.send(alert)
            setTimeout(() => {
              socket.close()
              MockServer.close()
            }, 1000)
          })
          return new WebSocket(url) /* mock socket  */
        })
      }
    })

@quentinus95
Copy link

quentinus95 commented Aug 31, 2019

Here is a more complete example. I've been using this for a year to test applications without any issue.

In a server.js file (for instance):

const sockets = {}
export function initServer() {
  // useful to reset sockets when doing TDD and webpack refreshes the app
  for (const socket of Object.values(sockets)) {
    socket.close()
  }

  mockServer()
}

function mockServer() {
  // Of course, your frontend will have to connecto to localhost:4000, otherwise change this
  sockets.mockServer = new Server("ws://localhost:4000")

  sockets.mockServer.on("connection", socket => {
    sockets.server = socket

    // Will be sent any time a client connects
    socket.send("Hello, world!")

    socket.on("message", data => {
      // Do whatever you want with the message, you can use socket.send to send a response
    }
  }
}

You can also use setInterval to send events regularly, and even export some functions to trigger an event from the server in a test.

You then just have to stub websocket in the visit Cypress method:

import { initServer } from "./server.js"

cy.visit("/", {
  onBeforeLoad(win) {
    initServer()
    cy.stub(win, "WebSocket", url => new MockSocket.WebSocket(url))
  }
})

That's all.

@tuzmusic
Copy link

@theAndrewCline thanks for that example, I've got a test working but it only works for one test, then it says that the mock server is already listening on that url:

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).on('connection', socket => {
            console.log('mock socket connected');
            mockSocket = socket;
          });
          if (!mockServer) return new WebSocket(url);
        });
      },
    });
  });

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

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

If I change the method to before() instead of beforeEach it works, but then I don't get a fresh environment for each test. I tried mockSocket.close() in afterEach() as you can see, but that doesn't work.

@quentinus95
Copy link

@tuzmusic you're only closing the socket but not stopping the server, have you tried to call .stop() on your server instance?

@shanysegal
Copy link

do we have to use cy.visit('/', {...}) and not specific url inside my app? why?

@tuzmusic
Copy link

do we have to use cy.visit('/', {...}) and not specific url inside my app? why?

We don't: you can set the baseUrl in your cypress.json file to the address where your app is being hosted (http://localhost:3000 for example) and then cy.visit uses paths relative to that.

So cy.visit('/') becomes equivalent to cy.visit('http://localhost:3000/') and cy.visit('/some-other-page.html') becomes equivalent to cy.visit('http://localhost:3000/some-other-page.html').

Without the baseUrl you'd need to include those entire urls.

@AB1519
Copy link

AB1519 commented Jan 18, 2020

I have an example that is almost working the way I want it to now. Having an issue with the Server I believe. for some reason, it will not close and tries to reconnect and cypress keeps picking up xhr requests so it doesn't close.

However, I was able to stub out the client web socket correctly.

cy.visit('/', {
      onBeforeLoad: (win) => {
        cy.stub(win, 'WebSocket', (url) => {
          MockServer = new Server(url)
          MockServer.on('connection', (socket) => {
            socket.send(alert)
            setTimeout(() => {
              socket.close()
              MockServer.close()
            }, 1000)
          })
          return new WebSocket(url) /* mock socket  */
        })
      }
    })

@tuzmusic I am trying to use the same code snippet, in my code, but I always get this error Server not defined.

@BobD
Copy link

BobD commented Mar 3, 2020

Just my two cents, this custom cypress command for graphQl websocket mocks works for me:

import { Server, WebSocket } from "mock-socket"

const mockGraphQlSocket = new Server(Cypress.env("GRAPHQL_WEB_SOCKET"))

Cypress.Commands.add("mockGraphQLSocket", mocks => {
  cy.on("window:before:load", win => {
    win.WebSocket = WebSocket
    mockGraphQlSocket.on("connection", socket => {
      socket.on("message", data => {
        const { id, payload } = JSON.parse(data)

        if (payload && mocks.hasOwnProperty(payload.operationName)) {
          mocks[payload.operationName](
            // Delegate the socket send call to the party owning the mock data since multiple calls might need to be made (for example to update multiple individual entries)
            data =>
              socket.send(
                JSON.stringify({
                  type: "data",
                  id,
                  payload: { errors: [], data }
                })
              ),
            payload.variables
          )
        }
      })
    })
  })
})

And then the command could be used something like this (depending on the query requirements)

cy.mockGraphQLSocket({
    yourGraphQlQuery: (send, { uuids }) => {
      uuids.forEach(id => {
        send({
          yourGraphQlQuery: {
            ...yourMockData,
            id
          }
        })
      })
    }
  })

@theAndrewCline
Copy link

@BobD looks nice. How do you close you mock server though?

@BobD
Copy link

BobD commented Mar 6, 2020

@theAndrewCline Good question, haven't though about that.

I guess the mock-socket close() can be used for that but i haven't hit a use-case where i needed that in my tests (yet) https://www.npmjs.com/package/mock-socket#server-methods

@maxime1992
Copy link
Author

I gave this another go today based on few comments above, using MockSocket. Absolutely no luck so far. Has anyone something to add that I could have missed or another solution?
Thanks

@robyn3choi
Copy link

Before instantiating a new Server, check if there's an existing one. If one exists, close it:

    if (mockServer) {
      mockServer.close();
    }
    mockServer = new Server('ws://localhost:8080');

@Alados
Copy link

Alados commented Jun 16, 2020

Does anyone work with AWS (AppSync) implementation of WebSockets? I can't connect mock, every time I'm getting an error: WebSocket connection to {url} failed

@erezcohen
Copy link

Anyone had any luck with mocking socket.io?
I was not able to intercept the ws using mock-socket approach, which is to add window.io = SocketIO;.

@ChristophWalter
Copy link

ChristophWalter commented Jul 2, 2020

@erezcohen I managed to mock socket.io using socktet.io-mock and custom commands.

Test

cy.mockSocketIO();
cy.visit("/");
cy.pushSocketIOMessage("chat", "Hello World");

Commands

import SocketMock from "socket.io-mock";
let socket = new SocketMock();

Cypress.Commands.add("mockSocketIO", (mocks) => {
  cy.on("window:before:load", (window) => {
    window.io = socket;
  });
});
Cypress.Commands.add("pushSocketIOMessage", (event, payload) => {
  socket.socketClient.emit(event, payload);
  setTimeout(() => {}, 1000);
});

Application

if (window.io) {
  this.socket = window.io;
} else {
  this.socket = io("/");
}

I am not happy with the application part. But could not find another way to mock the socket.io-client.

@edjumacator
Copy link

function mockServer() {
  // Of course, your frontend will have to connecto to localhost:4000, otherwise change this
  sockets.mockServer = new Server("ws://localhost:4000")

  sockets.mockServer.on("connection", socket => {
    sockets.server = socket

    // Will be sent any time a client connects
    socket.send("Hello, world!")

    socket.on("message", data => {
      // Do whatever you want with the message, you can use socket.send to send a response
    }
  }
}

@quentinus95 : Where is new Server() being defined in this example? Is the a specific dependency you need?

@quentinus95
Copy link

@edjumacator you can import it from the mock-socket library.

@bahmutov
Copy link
Contributor

I used the "socket.io-mock" when testing socket.io chat, here is a video https://www.youtube.com/watch?v=soNyOqpi_gQ

@mrpicklez70
Copy link

@bahmutov do you happen to know of any examples for mocking GraphQL Subscriptions?

@bahmutov
Copy link
Contributor

bahmutov commented Jul 5, 2021 via email

@kanteankit
Copy link

kanteankit commented Jul 28, 2021

I tried to use the code shown in this comment, but, it did not work for me. That's probably because Cypress queues it's commands. So, mockSocket remains undefined when mockSocket.send(JSON.stringify(object)); is queued. Read more about variables and aliases

I now have combined the code examples of these 2 comments and modified them according to the existing Cypress capabilities:

Here's the code that worked for me

// server.js

import { Server } from 'mock-socket'

export const getServer = () => {
    return new Cypress.Promise(resolve => {
        // Initialize server
        const mockServer = Server('wss://some-url-for-socket/')

        let mockSocket
        mockServer.on('connection', (socketHandle) => {
           resolve(socketHandle)
        })
    })
}
// Our e2e test file
import { getServer } from `server.js`
import { WebSocket } from 'mock-socket'

it('should send mock socket message', () => {
   
   // Create mock server
   const socketPromise =  getServer()
 
   // visit the page to establish connection
   cy.visit('/some-page-that-is-to-be-tested', {
      onBeforeLoad: (win) => {
         // Stub out JS WebSocket
         cy.stub(win, 'WebSocket', url => new WebSocket(url))
      }
   })
   
   // Resolve the Promise to get the server's socket handle
   // This is done after `visit` because that's when a connection would have been established
  cy.wrap(socketPromise).then((mockSocket) => {
      // Use the `mockSocket` variable to send a message to client
      mockSocket.send('Some message to the client')

      
      // After this, write code that asserts some change that happened due to the socket message
  })

})

Explanation for the code:

  • First, keep in mind that Cypress does not use normal JS promise. It uses Bluebird Promise. More details here
  • Wrap the server creation code in Cypress.Promise. Call resolve, once a connection is established.
  • In the e2e test, initialize the server Promise. After the URL is visited, client will establish connection with our dummy server.
  • Resolve the promise by calling cy.wrap. This will give us the socket handle of the dummy server
  • Now, use the socket handle to send message to the client.

@tochman
Copy link

tochman commented Jul 31, 2021

Hi, following this thread and wanted to throw in my recent finding in case it might help anyone... In a demo session with a few colleagues, we played around with web sockets and came up with a way to test messages sent on connection and incoming messages based on @kanteankit solution. #2492 (comment)

Demo code is available here: https://github.com/AgileVentures/wtw_ws_chat_demo

@petermanders89
Copy link

I've used the implementation from @tochman. However I ran into erros using the mock-socket library. I am using the fake-socket library. It is working without any problems now.

@VirusSniffer
Copy link

import { initServer } from "./server.js"

cy.visit("/", {
  onBeforeLoad(win) {
    initServer()
    cy.stub(win, "WebSocket", url => new MockSocket.WebSocket(url))
  }
})

@quentinus95 In this what is MockSocket and where it is imported from?

@dariusbhx
Copy link

@theAndrewCline
I am getting this error when I tried to send a websocket message using your example:
Cannot read properties of undefined (reading 'send')
Just a quick question as well are you using the mock-socket library

@jwedel
Copy link

jwedel commented Apr 4, 2022

Is this issue also related to just spying (using wait) and not stubbing/changing the WS behavior?

When I try to intercept the http GET to a websocket and then wait for it, it does not work and times out.

cy.intercept({
 url: '**/websocket*',
 method: 'GET',
}).as('wsConnect');

Navigator.visit('/some/url');

cy.wait('@wsConnect');

Use case:
For our page, I need to wait until the websocket is established which takes a bit because after that I now elements will appear in a table. When not waiting, other guards fail.

@cypress-bot cypress-bot bot added stage: icebox and removed stage: proposal 💡 No work has been done of this issue labels Apr 28, 2022
@raarts
Copy link

raarts commented May 14, 2022

@mrpicklez70 Did you succeed in mocking graphql subscrioptions?

@addy
Copy link

addy commented Jun 15, 2022

I've been having a hell of a time getting this pattern working. I have multiple tests within the same file all invoking a similar pattern to what @kanteankit posted, but my tests kept bombing out seemingly because I had consecutive tests using the pattern.

I was seeing errors similar to this when running in headless mode. cypress open was running just fine.

What ultimately fixed my issue was passing the --browser chrome flag to my cypress run command. Hope this helps someone.

@xsqox
Copy link

xsqox commented Jul 27, 2022

@bahmutov asking you as I was unsuccessful resolving the issue. Hoping you will have some insights.

I have an app that uses both XHR requests and web socket connections (mainly to receive messages from the server). Web socket connection is implemented with socket.io so i managed to mock web socket with socket.io-mock..

However, I am seeing a weird behavior where mocked message (sent by the server) is not being processed by the socket client ONLY IF routes are not intercepted. If the routes are intercepted, message is getting processed as expected. As soon as I remove command to stub the requests, message disappears.

Here is my setup:

Websocket mocking

mport SocketMock from 'socket.io-mock';

const socket = new SocketMock();

Cypress.Commands.add('mockSocketIO', () => {
    cy.on('window:before:load', (window) => {
        window.io = socket;
        setTimeout(() => {}, 2000);
    });
});
Cypress.Commands.add('pushSocketIOMessage', (event, payload) => {
    socket.socketClient.emit(event, payload);
    setTimeout(() => {}, 2000);
});

Code to make sure that socket is replaced with mocked socket if running cypress

   if (window.io) {
        // if in cypress context
        socket = window.io; // executes correctly both with and without stubbed routes
    } else {
        socket = io(payload.socketUrl, {
            auth: { token: payload.accessToken }
        });
    }

Cypress test

describe('Basic socket handling', () => {
    beforeEach(() => {
        cy.mockSocketIO();
    });

    describe('it should properly process unknown message', () => {
        beforeEach(() => {
            cy.setTime('2022-07-21');
            cy.stubHomePageAfterLogin(); // if removed out the websocket message won't work
        });

        it('should login and redirect to home page when logged in', () => {
            cy.loginWithCredentials();
            cy.url().should('contain', '/home');
            cy.pushSocketIOMessage('message', {
                message: 'unknown-message'
            });
        });
   })
})

cy.stubHomePageAfterLogin command has just a bunch of cy.intercept commands like:

Cypress.Commands.add('stubHomePageAfterLogin', () => {
    cy.intercept(
        {
            method: 'POST', 
            url: 'https://cognito-idp.us-east-1.amazonaws.com/'
        },
        { fixture: 'cognito.json' }
    ).as('authUser');
    cy.intercept(
        {
            method: 'GET', 
            url: `/user`
        },
        { fixture: 'user.json' }
    ).as('getUser');
    cy.intercept(
        {
            method: 'GET',
            url: `/metrics`
        },
        { fixture: 'metrics.json' }
    ).as('getMetrics');

    cy.intercept(
        {
            method: 'GET',
            url: `${apiBaseUrl}/clients?*`
        },
        { fixture: 'clients.json' }
    ).as('getClients');
    cy.intercept(
        {
            method: 'GET',
            url: `/address/*`
        },
        { fixture: 'address.json' }
    ).as('getAddress');
});

@coco1979ka
Copy link

Thanks to the help in this thread, I was able to write a Cypress plugin to deal with web sockets. You can check it out here: https://www.npmjs.com/package/cypress-mock-websocket-plugin

Any feedback appreciated - especially if it's kind :)

@JasonLandbridge
Copy link

For those looking for a way to make SignalR work with Cypress: Cypress-SignalR-Mock

Any feedback is greatly appreciated!

@nagash77 nagash77 added the E2E Issue related to end-to-end testing label May 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
E2E Issue related to end-to-end testing topic: cy.intercept() type: feature New feature that does not currently exist
Projects
None yet
Development

No branches or pull requests