Test WebSockets in Vitest with MSW

Let's write an integration test for our <Chat/> component in Vitest and React Testing Library.

The WebSocket interception in MSW requires a global WebSocket class. You are covered if you're using Node.js v22 or higher since that global class exists there. But for older versions, we will create a custom Vitest environment to polyfill it using undici.

Resources

Share with a coworker

Social Share Links

Transcript

[00:00] Here's what you need to test your chat application in integration level test with VDest. Let's start from the setup. Since we're using Remix that runs on feet, we can reuse the same vidconfig.ts in our test runner as well. This will give us the same plugins such as TypeScript path aliases and any other things we use in our code automatically be available during the test run. And if you need to customize the test framework itself, include this test property in your ViteConfig and provide the configuration that Vitest expects.

[00:31] Like right here, I'm telling Vitest to expose its functions as globals, so things like test, expect, before all, and so forth. I'm also specifying a path to a custom setup file, as well as a custom environment. So let's talk about those in more detail. MSW expects your environment to have a global WebSocket class, which is available in Node.js since version 22. But if you're running on older versions of Node.js, you can create a custom VTest environment to include that WebSocket class by yourself.

[01:00] So here we are providing the path to this custom VTest environment and let's see how it's implemented. Custom environment is just an object that satisfies this environment type from VTest that includes the most important method which is a setup method. In this method we will extend the built-in HappyDOM environment, so make sure to have it installed, and we will expose a global WebSocket property using the WebSocket class from Undiji. Undiji is an official implementation of Fetch and WebSocket in Node.js, and you can install it as a regular dependency as well. So here we're grabbing the WebSocket class, which is the same class that's been run in Node.js since version 22.

[01:38] Now that we've exposed this WebSocket class, our tests and the code that we're testing can create new WebSocket instances in the same way that it does in the browser. The next step is to create a Vitas setup file. Here in vitas.setup.ts, this is a regular Node.js integration for MSW. So we have a server instance right here, and we're using hooks like before all, after each and after all, to establish the interception, reset the runtime handlers that we may add in individual tests, and also stop the request interception once our tests are done. This server instance itself is just a result of calling setupServer with the request handlers, or event handlers in our case, that will act as happy path behaviors.

[02:20] For the purpose of this exercise, I actually went to the handlers and cleared any event handlers that we had before. This means that our test will have no happy path behaviors and we will add behaviors in each individual test. The way you structure handlers is absolutely up to you and you can refer to the MSW docs to learn more about that. Since automated tests allow us to replay different behaviors in our chat, it would be nice to export this chat websocket link so we're able to access it in tests and provision any behavior overrides. Now let's take a look at the test itself.

[02:54] So I have this chat.test.tsx file and right now it has everything we need to test our chat application. Let's write a simple test case that verifies that the message we sent in the chat actually appears visible in the list of messages. I will start from rendering our chat component. I will call render from React testing library and provide the chat component right here. The chat component expects the user prop which is the currently authenticated user.

[03:22] I'm gonna paste this mock user right here and click Save. Next let's grab our message input by creating a variable called message input that will be the result of calling screen get by label text chat message. And finally let's get a send button by locating it by its role, get by role, button, and also the name that will say send. Now let's interact with the chat. Let's send a message by calling event type into the message input and we're going to type the message hello world and finally let's submit this message by calling click on the send button.

[04:06] Now that we've sent our message in test, we expect it to appear in the list of messages for the user. To do that, let's create an assertion that awaits screen bind by role log, which will be our chat message, to have text content hello world. This concludes the action and the assertion phases of this test but there's one thing we're still missing, the setup phase. We need to describe the WebSocket behavior in this particular scenario. To do that let's head at the beginning of the test right before render, reference our server which is the MSW server and call use.

[04:45] This will prevent any behavior overrides that will take place in the network for this particular test. So here let's use the chat web socket link, add the connection listener, grab the client object, then listen to the message event on the client, which is going to happen when we're going to click submit in our test, add this listener, and just forward this event back to the client. In this test, MSW will act as an actual server by listening to outgoing messages and sending them back as a confirmation to any sent chat message. And finally let's verify our task passing by opening it in the terminal and running VDust. So what's happening here is that we're modeling a particular use case here.

[05:31] We're rendering the chat component, and we enter a particular message, submit it in the chat, and we expect that message to appear in the UI, which means it's visible for all the users.