I want to unit test this JS code with Jest. It’s using the ws websocket library:
Socket.js
import WebSocket from "ws"; export default class Socket { socket = undefined; connect(url): void { if (this.socket !== undefined && this.socket.readyState === WebSocket.OPEN) { throw new Error("Socket is already connected"); } this.socket = new WebSocket(url); } }
I want to check the error is thrown. For that to happen, I create a mock for the socket and then need to set the readyState
of the socket object to CLOSED
. The issue is that this is impossible because it’s a readonly field.
Test:
import Socket from "./Socket"; import WebSocket from "ws"; jest.mock("ws"); it("can connect", () => { // Arrange const url = "www.someurl.com"; jest.spyOn(global, "Promise").mockReturnValueOnce({} as Promise<void>); const sut = new Socket(); // Act sut.connect(url); const webSocket = jest.mocked(WebSocket).mock.instances[0]; // Set readystate of socket webSocket.readyState = WebSocket.CLOSED; // Error here: Cannot assign to 'readyState' because it is a read-only property. ts(2540) const action = () => sut.connect(url); // Assert expect(action).toThrowError(new Error("Socket is already connected")); });
Question: How to test any code that uses readonly fields on mocks of libraries such as socket.readyState
?
Edit: Added the mvce on request above.
Advertisement
Answer
The better test strategy is to create a test WebSocket server and connect it. This is also the official team doing, see some test cases. Mocking ws
and testing the implementation details is not recommended.
Below testing code using TypeScript:
socket.ts
:
import WebSocket from "ws"; export default class Socket { socket!: WebSocket; connect(url: string): void { if (this.socket !== undefined && this.socket.readyState === WebSocket.OPEN) { throw new Error("Socket is already connected"); } this.socket = new WebSocket(url); } }
socket.test.ts
:
import WebSocket from "ws"; import Socket from "./socket"; describe('74062437', () => { test('should connect the websocket server', (done) => { const wss = new WebSocket.Server({ port: 0 }, () => { const socket = new Socket() socket.connect(`ws://localhost:${(wss.address() as WebSocket.AddressInfo).port}`); socket.socket.on('open', () => { expect(socket.socket.readyState).toEqual(WebSocket.OPEN); socket.socket.close(); }); socket.socket.on('close', () => wss.close(done)); }); }); test('should throw error if already connected websocket server', (done) => { expect.assertions(2); const wss = new WebSocket.Server({ port: 0 }, () => { const socket = new Socket(); socket.connect(`ws://localhost:${(wss.address() as WebSocket.AddressInfo).port}`); socket.socket.on('open', () => { expect(socket.socket.readyState).toEqual(WebSocket.OPEN); expect(() => socket.connect(`ws://localhost:${(wss.address() as WebSocket.AddressInfo).port}`)).toThrowError('Socket is already connected') socket.socket.close(); }); socket.socket.on('close', () => wss.close(done)); }); }); });
Test result:
PASS stackoverflow/74062437/socket.test.ts (10.964 s) 74062437 ✓ should connect the websocket server (31 ms) ✓ should throw error if already connected websocket server (6 ms) -----------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -----------|---------|----------|---------|---------|------------------- All files | 100 | 100 | 100 | 100 | socket.ts | 100 | 100 | 100 | 100 | -----------|---------|----------|---------|---------|------------------- Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 11.784 s
package versions:
"ws": "^8.9.0", "jest": "^26.6.3"