How to mock socket.io-client using jest/react-testing-library
Asked Answered
B

4

5

I am building a chat app and would like to write integration tests using react-testing-library and can not figure out how to mock socket.io-client's socket.on, socket.emit, etc.

I tried follow this article and tried using mock-socket.io and socket.io-mock all with no luck.

This is the component I am trying to test:

import React, { useEffect, useState } from 'react';
import io from 'socket.io-client';
import 'dotenv/config';
import ChatInput from './ChatInput';
import Messages from './Messages';

function App() {
  const [messages, setMessages] = useState([]);

  const port = process.env.REACT_APP_SERVER_PORT;
  const socket = io(`http://localhost:${port}`);

  useEffect(() => {
    socket
      .emit('app:load', messageData => {
        setMessages(messages => [...messages, ...messageData]);
      })
      .on('message:new', newMessage => {
        setMessages(messages => [...messages, newMessage]);
      });
  }, []);

  const postMessage = input => {
    socket.emit('message:post', input);
  };

  return (
    <div className="App">
      <Messages messages={messages} />
      <ChatInput postMessage={postMessage} />
    </div>
  );
}

export default App;
Bradwell answered 3/10, 2019 at 2:44 Comment(0)
I
11

This is a late answer but maybe useful for others:

to mock socket.io-client library I used jest mock function and used a third party library socket.io-mock https://www.npmjs.com/package/socket.io-mock

You need to modify your connection function as follows in order to work with mocked socket:

const url= process.env.NODE_ENV==='test'?'':`http://localhost:${port}`;
const socket = io(url);

Implementation:

import socketIOClient from 'socket.io-client';
import MockedSocket from 'socket.io-mock';

jest.mock('socket.io-client');

describe('Testing connection', () => {
  let socket;

  beforeEach(() => {
    socket = new MockedSocket();
    socketIOClient.mockReturnValue(socket);
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('should dispatch connect event', () => {
    /*socket should connect in App and 
    Note that the url should be dummy string 
    for test environment e.g.(const socket = io('', options);)*/
    const wrapper = (
      <Provider store={store}>
        <App />
      </Provider>
    );

    expect(socketIOClient.connect).toHaveBeenCalled();
  });

  it('should emit message:new', done  => {
    const wrapper = (
      <Provider store={store}>
        <App />
      </Provider>
    );
    ...
    socket.on('message:new', (data)=>{
        expect(data).toEqual(['message1', 'message2']);
        done();
    });

    socket.socketClient.emit('message:new', ['message1', 'message2']);
    ...
  });
});
Iridissa answered 29/4, 2020 at 12:33 Comment(4)
tried to follow this, got into "_socket2.default.mockReturnValue is not a function" error from jest. seems like the jest.mock('socket.io-client') isn't always working. maybe version issues?Ample
And how can you mock the socket server in this setup?Mighell
it's not working for me.` socketIOClient.mockReturnValue(socket); ` is throwing me errorDisk
This worked for me except I used the typescript equivalent, 'socket.io-mock-ts'Phonograph
K
0

I also tried a couple of 3rd party libraries which were outdated or didn't work. Then I realized that mocking client interface using jest tools isn't that hard.

The following code snippet describes the idea and should work for your case. Or it might provide some clue for others looking for ways to mock socket.io client.

The trick is to memoize on handlers on mount and later trigger them per test case.

import { io } from 'socket.io-client';

const eventHandlers = {};
const mockSocketConnect = jest.fn();
const mockSocketDisconnect = jest.fn();
const mockSocketEmit = jest.fn();

jest.mock('socket.io-client', () => ({
  __esModule: true,
  io: () => ({
    connect: mockSocketConnect,
    disconnect: mockSocketDisconnect,
    emit: mockSocketEmit,
    on: (event, handler) => {
      eventHandlers[event] = handler;
    },
  }),
}));

const triggerSocketEvent = (event, data) => {
  if (eventHandlers[event]) {
    eventHandlers[event](data);
  }
};

describe('App', () => {
  it('should set messages on new message event', () => {
    // render App component
    triggerSocketEvent('message:new', 'Hello World');
    // expect messages to be updated
  });

  it('should emit message post event', () => {
    // render App component
    // emulate message input change
    expect(io().emit).toHaveBeenCalledWith('message:post', 'Hello World');
  });
});

Kiangsu answered 30/5, 2024 at 14:38 Comment(0)
D
0

I was not able to find any luck with socket.io-mock. My assumption is that it might have become deprecated with some newer version of socket.io, having been last updated 3 years ago.

I found much more success following this article:

https://www.bomberbot.com/testing/testing-socket-io-client-apps-a-comprehensive-guide/

Mock Setup

They define the socket.io-client mock in a jest.setup.js file. This is how I got it to work, however I am not 100% sure that putting the mock in the jest.setup.js is required.

jest.setup.js:

jest.mock('socket.io-client', () => {
    const emit = jest.fn();
    const on = jest.fn();
    const off = jest.fn();
    const socket = { emit, on, off };
    return {
        io: jest.fn(() => socket),
    }
});

jest.config.js:

module.exports = {
  ...
  setupFilesAfterEnv: [‘<rootDir>/jest-setup.js‘],
  ...
};

Test Incoming Messages

MyComponent.test.tsx:

import { render, screen, waitFor } from ‘@testing-library/react‘;
import { io as mockIo } from ‘socket.io-client‘;
import MyComponent from ‘./MyComponent‘;

describe(‘MyComponent‘, () => {
  it(‘should render incoming messages‘, async () => {
    render(<MyComponent />);

    // wait for io() to be called: client initialized
    await waitFor(() => {
      expect(mockIo).toHaveBeenCalled();
    });

    const socket = mockIo.mock.results[0].value;

    // mock an incoming message and add it to the queue
    act(() => {
      socket.on.mock.calls.find(([event]) => event === ‘message‘)?.[1]({
        id: ‘1‘,
        text: ‘Hello world!‘,  
        sender: ‘Alice‘,
        timestamp: Date.now(),
      });  
    });

    // test that message content is displayed
    expect(screen.getByText(‘Hello world!‘)).toBeInTheDocument();
    expect(screen.getByText(‘Alice‘)).toBeInTheDocument();
  });
});

Test Outgoing Messages

MyComponent.test.tsx ...continued:

...

it(‘should send messages and display delivery status‘, async () => {
  render(<MyComponent />);

  const messageInput = screen.getByPlaceholderText(‘Enter message‘);
  const sendButton = screen.getByRole(‘button‘, { name: /send/i });

  userEvent.type(messageInput, ‘Hello!‘);
  userEvent.click(sendButton);

  expect(screen.getByText(‘Hello!‘)).toBeInTheDocument();

  expect(screen.getByLabelText(‘Sending‘)).toBeInTheDocument();

  // look for calls to socket.emit() and test call arguments
  const socket = mockIo.mock.results[0].value;
  const emitCalls = socket.emit.mock.calls;  
  const [topic, msg] = emitCalls[0];

  expect(topic).toEqual("message");
  expect(msg).toEqual(expect.objectContaining({ 
    text: ‘Hello!‘ 
  }));

  // mock a server response message to confirm that the initial message was delivered
  act(() => {
    socket.on.mock.calls.find(([event]) => event === ‘message-delivered‘)?.[1]({
      id: ‘2‘,  
    });
  });

  await waitFor(() => {
    expect(screen.queryByLabelText(‘Sending‘)).not.toBeInTheDocument();
  });

  expect(screen.getByLabelText(‘Delivered‘)).toBeInTheDocument();
});
...
D answered 8/7, 2024 at 15:22 Comment(0)
S
0

I have been trying to implement Jest mocks for socket-io.client and followed @Ty Riviere way of doing it. But the line import {io as mockIo} from 'socket-io.client was not being treated as mocks by Jest, even though the jest.setup.js and jest.config.js was set up as mentioned.

Manually Mocking Socket-io.client

something.test.tsx:

...

let mockIo: jest.Mock;

jest.mock(`socket.io-client`, () => {
    const emit = jest.fn();
    const on = jest.fn();
    const off = jest.fn();
    const socket = {emit, on, off};
    mockIo = jest.fn(() => socket);
    return {io: mockIo};
})

... // can reference mockIo later on for instance, mockIo.mock.results[0]
Scriber answered 20/9, 2024 at 21:31 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.