- 1. JavaScript testing #1. Explaining types of tests. Basics of unit testing with Jest
- 2. JavaScript testing #2. Introducing Enzyme and testing React components
- 3. JavaScript testing #3. Testing props, the mount function and snapshot tests.
- 4. JavaScript testing #4. Mocking API calls and simulating React components interactions
- 5. JavaScript testing #5. Testing hooks with react-hooks-testing-library and Redux
- 6. JavaScript testing #6. Introduction to End-to-End testing with Cypress
- 7. JavaScript testing #7. Diving deeper into commands and selectors in Cypress
- 8. JavaScript testing #8. Integrating Cypress with Cucumber and Gherkin
- 9. JavaScript testing #9. Replacing Enzyme with React Testing Library
- 10. JavaScript testing #10. Advanced mocking with Jest and React Testing Library
- 11. JavaScript testing #11. Spying on functions. Pitfalls of not resetting Jest mocks
- 12. JavaScript testing #12. Testing downloaded files and file inputs with Cypress
- 13. JavaScript testing #13. Mocking a REST API with the Mock Service Worker
- 14. JavaScript testing #14. Mocking WebSockets using the mock-socket library
- 15. JavaScript testing #15. Interpreting the code coverage metric
- 16. JavaScript testing #16. Snapshot testing with React, Jest, and Vitest
- 17. JavaScript testing #17. Introduction to End-to-End testing with Playwright
- 18. JavaScript testing #18. E2E Playwright tests for uploading and downloading files
Every automated test that we write should be deterministic. One of the things we need to do to achieve that is to make our frontend tests independent of the backend. Besides mocking REST API requests, we also need to learn how to mock a WebSocket connection. In this article, we create a simple backend application and use React to connect to it and render data. We also write unit tests that ensure that our frontend application works correctly.
Creating a simple application with WebSockets
Let’s start by defining a simple application that uses WebSockets:
- the frontend application establishes a connection with the WebSocket server and sends a LogsRequested message,
- the server receives the LogsRequested and starts sending logs,
- the server sends the NoMoreLogs message if there are no more logs,
- the frontend application closes the WebSocket connection as soon as it receives the NoMoreLogs message.
First, we need to define the format of the messages our WebSocket server sends.
ServerMessage.ts
1 2 3 4 5 6 7 8 9 10 |
export enum ServerMessageType { Log = 'Log', NoMoreLogs = 'NoMoreLogs', } export interface ServerMessage { id: number; type: ServerMessageType; content: string; } |
Besides the above, we also need to specify the messages that our frontend application can send.
ClientMessage.ts
1 2 3 4 5 6 7 |
export enum ClientMessageType { LogsRequested = 'LogsRequested', } export interface ClientMessage { type: ClientMessageType; } |
Feel free to add more types of message types if your applicatio needs it.
Creating the WebSocket server
Once we have the messages defined, we can create the WebSocket server we will mock later.
server.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import { Server } from 'ws'; import { ServerMessage, ServerMessageType } from './ServerMessage'; import { ClientMessage, ClientMessageType } from './ClientMessage'; const webSocketServer = new Server({ port: 5000, }); const messages: ServerMessage[] = [ { id: 1, type: ServerMessageType.Log, content: 'Log #1', }, { id: 2, type: ServerMessageType.Log, content: 'Log #2', }, { id: 3, type: ServerMessageType.NoMoreLogs, content: 'There will be no more logs', }, ]; webSocketServer.on('connection', (socket) => { socket.on('message', (message) => { const messageData: ClientMessage = JSON.parse(message.toString()); if (messageData.type === ClientMessageType.LogsRequested) { messages.forEach((message) => { socket.send(JSON.stringify(message)); }); } }); }); |
If you want to know more about WebSockets, check out Introduction to WebSockets. Creating a Node.js server and using WebSocket API in the browser
Above, we send all the messages one by one as soon as the client sends the LogsRequested message.
Creating the frontend application
In our frontend application, we aim to receive all of the logs and render them.
LogsList.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import React from 'react'; import useLogs from './useLogs'; const LogsList = () => { const { messages } = useLogs(); return ( <div> {messages.map((message) => ( <div key={message.id}>{message.content}</div> ))} </div> ); }; export default Logs; |
To connect to the server, we need to add its URL to the environment variables.
.env
1 |
REACT_APP_WEB_SOCKET_URL=ws://localhost:5000 |
In this simple example, we use Create React App. With CRA, all custom environment variables have to start with REACT_APP.
For the above variable to work well with TypeScript, let’s declare it in the react-app-env.d.ts file.
react-app-env.d.ts
1 2 3 4 5 |
declare namespace NodeJS { interface ProcessEnv { REACT_APP_WEB_SOCKET_URL: string; } } |
Under the hood of our component, we want to connect to the WebSocket server and receive the logs.
useLogsList.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import { useEffect, useState } from 'react'; import { ServerMessage, ServerMessageType } from './ServerMessage'; import { ClientMessage, ClientMessageType } from './ClientMessage'; function useLogsList() { const [messages, setMessages] = useState<ServerMessage[]>([]); const handleConnection = (webSocket: WebSocket) => { const requestLogsMessage: ClientMessage = { type: ClientMessageType.LogsRequested, }; webSocket.send(JSON.stringify(requestLogsMessage)); }; const handleIncomingMessage = ( webSocket: WebSocket, message: MessageEvent, ) => { const messageData: ServerMessage = JSON.parse(message.data); setMessages((currentMessages) => { return [...currentMessages, messageData]; }); if (messageData.type === ServerMessageType.NoMoreLogs) { webSocket.close(); } }; useEffect(() => { const webSocket = new WebSocket(process.env.REACT_APP_WEB_SOCKET_URL); setMessages([]); webSocket.addEventListener('open', () => { handleConnection(webSocket); }); webSocket.addEventListener('message', (message) => { handleIncomingMessage(webSocket, message); }); return () => { webSocket.close(); }; }, []); return { messages, }; } export default useLogsList; |
Doing the above causes our React application to establish a WebSocket connection and render all the logs.
Testing our frontend application with mock-socket
To test our frontend application, we must first install the mock-socket library.
1 |
npm install mock-socket |
To be able to mock the WebSocket connection, we need to create an instance of the Server.
1 2 3 |
import { Server } from 'mock-socket'; const websocketServer = new Server('ws://localhost:5000'); |
The mock-socket library also has the socket.io support.
The Server class mimics the WebSocket API we’ve used in Node.js. Thanks to that, it is straightforward to use.
1 2 3 4 5 6 |
webSocketServer.on('connection', (socket) => { socket.on('message', (message) => { console.log('Received a message from the client', message); }); socket.send('Sending a message to the client'); }); |
Once we don’t need our server anymore, we can call websocketServer.close().
Let’s write a simple test combining all of the above knowledge.
LogsList.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
import { Server } from 'mock-socket'; import { ClientMessage, ClientMessageType } from './ClientMessage'; import { ServerMessage, ServerMessageType } from './ServerMessage'; import LogsList from './LogsList'; import { render, waitFor } from '@testing-library/react'; describe('The LogsList component', () => { let websocketServer: Server; let serverMessages: ServerMessage[]; beforeEach(() => { serverMessages = []; websocketServer = new Server(process.env.REACT_APP_WEB_SOCKET_URL); websocketServer.on('connection', (socket) => { socket.on('message', (message) => { if (typeof message !== 'string') { return; } const parsedData: ClientMessage = JSON.parse(message); if (parsedData.type === ClientMessageType.LogsRequested) { serverMessages.forEach((message) => { socket.send(JSON.stringify(message)); }); } }); }); }); afterEach(() => { websocketServer.close(); }); describe('if the WebSocket sends messages and announces the end of logs', () => { beforeEach(() => { serverMessages = [ { id: 1, type: ServerMessageType.Log, content: 'Log #1', }, { id: 2, type: ServerMessageType.Log, content: 'Log #2', }, { id: 3, type: ServerMessageType.NoMoreLogs, content: 'There will be no more logs', }, ]; }); it('should render all of the logs', async () => { const logsList = render(<LogsList />); for (const message of serverMessages) { await logsList.findByText(message.content); } }); }); }); |
PASS src/LogsList/LogsList.test.tsx
The LogsList component
if the WebSocket sends messages and announces the end of logs
✓ should render all of the logs
Testing if the WebSocket connection was closed
We want our frontend application to close the WebSocket connection as soon as it receives the NoMoreLogs message.
To test the above, we can take advantage of the clients() function that returns the array of all connected clients.
LogsList.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
describe('if the WebSocket sends messages and announces the end of logs', () => { beforeEach(() => { serverMessages = [ { id: 1, type: ServerMessageType.Log, content: 'Log #1', }, { id: 2, type: ServerMessageType.Log, content: 'Log #2', }, { id: 3, type: ServerMessageType.NoMoreLogs, content: 'There will be no more logs', }, ]; }); it('should close the WebSocket connection', () => { render(<LogsList />); return waitFor(() => { return expect(websocketServer.clients().length).toBe(0); }); }); // ... }); |
PASS src/LogsList/LogsList.test.tsx
The LogsList component
if the WebSocket sends messages and announces the end of logs
✓ should render all of the logs
✓ should close the WebSocket connection
Above, we use the waitFor function because our code is asynchronous, and the application doesn’t close the connection immediately.
There is another case in which we want our application to close the WebSocket connection. We want it to happen when the application doesn’t receive the NoMoreLogs message, but the component is unmounted.
LogsList.test.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
describe('if the WebSocket sends messages and does not announce the end of logs', () => { beforeEach(() => { serverMessages = [ { id: 1, type: ServerMessageType.Log, content: 'Log #1', }, { id: 2, type: ServerMessageType.Log, content: 'Log #2', }, ]; }); it('should render all of the logs', async () => { const logsList = render(<LogsList />); for (const message of messages) { await logsList.findByText(message.content); } }); describe('and when the component is unmounted', () => { it('should close the WebSocket connection', () => { const logsList = render(<LogsList />); logsList.unmount(); return waitFor(() => { return expect(websocketServer.clients().length).toBe(0); }); }); }) }); |
PASS src/LogsList/LogsList.test.tsx
The LogsList component
if the WebSocket sends messages and does not announce the end of logs
✓ should render all of the logs
and when the component is unmounted
✓ should close the WebSocket connection
Testing the messages sent by the client
We might want to test if the frontend application sends the correct messages to our WebSocket server. To do the above, we can declare an array of the client messages and update it when the message arrives.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import { Server } from 'mock-socket'; import { ClientMessage, ClientMessageType } from './ClientMessage'; import { ServerMessage, ServerMessageType } from './ServerMessage'; import LogsList from './LogsList'; import { render, waitFor } from '@testing-library/react'; describe('The LogsList component', () => { let websocketServer: Server; let serverMessages: ServerMessage[]; let clientMessages: ClientMessage[]; beforeEach(() => { serverMessages = []; clientMessages = []; websocketServer = new Server(process.env.REACT_APP_WEB_SOCKET_URL); websocketServer.on('connection', (socket) => { socket.on('message', (message) => { if (typeof message !== 'string') { return; } const parsedData: ClientMessage = JSON.parse(message); clientMessages.push(parsedData); if (parsedData.type === ClientMessageType.LogsRequested) { serverMessages.forEach((message) => { socket.send(JSON.stringify(message)); }); } }); }); }); afterEach(() => { websocketServer.close(); }); it('should send the message requesting the logs', () => { render(<LogsList />); return waitFor(async () => { await expect(clientMessages.length).toBe(1); await expect(clientMessages[0].type).toBe( ClientMessageType.LogsRequested, ); }); }); // ... }); |
Summary
In this article, we’ve created a simple Node.js server and a frontend application that connects to it with a WebSocket connection. We’ve tested if the React application correctly interprets the messages sent by the server. Besides that, we’ve also tested if the client sends the expected messages and closes the connection when appropriate. All of the above gave us a solid grasp on how to use the mock-socket library to test frontend applications that use a WebSocket connection.