socketio / socket.io

Realtime application framework (Node.JS server)
https://socket.io
MIT License
60.93k stars 10.1k forks source link

Tests not work when use Timer Mocks like `jest.useFakeTimers()` #4534

Open MiniSuperDev opened 1 year ago

MiniSuperDev commented 1 year ago

Hello, when I use jest.useFakeTimers(), I don't know how long I have to advance the timers or what method to call so that the internal code that sends and receives socket.io events will be executed

I want to know what I should do, because I want to increase the execution time of the tests, and since I have to write tests for some features that use setTimeouts, and some RxJs methods like debounce, throttle, this is essential for me.

This is a minimal example of jest with typescript.

jest.useRealTimers();

const {createServer} = require('http');
import {Server, Socket} from 'socket.io';
import {Socket as ClientSocket, io as ioc} from 'socket.io-client';

describe('my awesome project', () => {
  let io: Server;

  let serverSocket: Socket;
  let clientSocket: ClientSocket;

  beforeAll(done => {
    const httpServer = createServer();
    io = new Server(httpServer);
    httpServer.listen(() => {
      const port = httpServer.address().port;
      clientSocket = ioc(`http://localhost:${port}`);
      io.on('connection', socket => {
        serverSocket = socket;
      });
      clientSocket.on('connect', done);
    });
  });

  afterAll(() => {
    jest.useRealTimers();
    io.close();
    clientSocket.close();
  });

  test('should work', async () => {
    jest.useFakeTimers();

    const onHello = jest.fn();

    clientSocket.on('hello', onHello);

    serverSocket.emit('hello', 'world');

    jest.advanceTimersByTime(10000);
    jest.runAllTicks();
    await new Promise(jest.requireActual('timers').setImmediate);
    expect(onHello).toBeCalledTimes(1);
    // Error
    // Expected number of calls: 1
    // Received number of calls: 0
  });
});

And this is the way how I write test currently without Timer Mocks, but I need to await a setTimeout promise with N milliseconds, to make it work, the problem is this time is variable, for example if I set 4 milliseconds, sometimes the test pass and other times fail, I think is based on my pc resources and the amount of tests that are running. But without fake timers this increment the total execution time, and I can't calculate how long to wait for when I want to wait for other methods to execute, for example from rxjs debounceTime or throttleTime

jest.useRealTimers();

const {createServer} = require('http');
import {Server, Socket} from 'socket.io';
import {Socket as ClientSocket, io as ioc} from 'socket.io-client';

describe('my awesome project', () => {
  let io: Server;

  let serverSocket: Socket;
  let clientSocket: ClientSocket;

  beforeAll(done => {
    const httpServer = createServer();
    io = new Server(httpServer);
    httpServer.listen(() => {
      const port = httpServer.address().port;
      clientSocket = ioc(`http://localhost:${port}`);
      io.on('connection', socket => {
        serverSocket = socket;
      });
      clientSocket.on('connect', done);
    });
  });

  afterAll(() => {
    io.close();
    clientSocket.close();
  });

  test('should work', async () => {
    const onHello = jest.fn();

    clientSocket.on('hello', onHello);

    serverSocket.emit('hello', 'world');

    await sleep(4);
    // sometimes pass and sometimes fail
    // with 4 milliseconds because this time is not enough time.
    //
    // So how many time should I wait?
    //
    // But without fake timers this increment total execution time,
    // and I can't calculate how long to wait for
    // when I want to wait for other methods to execute,
    // for example from rxjs debounceTime or throttleTime
    expect(onHello).toBeCalledTimes(1);
  });
});

function sleep(ms: number) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}

Thank you


    "socket.io": "^4.5.3",
    "socket.io-client": "^4.5.4"
    "jest": "^29.2.2",

Platform:

janeklb commented 1 year ago

+1 Using jest.useFakeTimers() in tests is also causing problems for me. Aside from updating socketio to address this, it would be great to get some guidance in the form of workarounds (that could be applied to projects using the current version of socketio).

darrachequesne commented 1 year ago

Hi! I don't think useFakeTimers() is meant to be used with I/O operations, is it?

The following method might be useful:

function waitFor(emitter, event) {
  return new Promise((resolve) => {
    emitter.once(event, resolve);
  });
}

Usage:

test('should work', async () => {
  serverSocket.emit('hello', 'world');

  await waitFor(clientSocket, "hello");
});

Reference: https://jestjs.io/docs/timer-mocks

darrachequesne commented 1 year ago

Closed due to inactivity, please reopen if needed.

aarowman commented 1 year ago

I am seeing this issue too - can we reopen? Or at least provide a workaround

darrachequesne commented 1 year ago

@aarowman could you please explain which kind of tests you would like to write? As I said above, I don't think fake timers are meant to be used with async operations such as HTTP requests.

janeklb commented 1 year ago

Hi! I don't think useFakeTimers() is meant to be used with I/O operations, is it?

Hi @darrachequesne, I'm curious to know why useFakeTimers() shouldn't be used with I/O operations -- could you please elaborate on that a bit?

The waitFor suggestion (thank you 🙇🏻) may work for certain kinds of tests, but definitely not all. There's a related, but imo separate, conversation to be had about whether code that requires those kinds of tests should be re-written in a way that eliminates that need; however, I still think it's valuable to consider what it would take to enable testing of code that uses socket.io in combination with jest.useFakeTimers().

darrachequesne commented 1 year ago

My understanding is that one would need to mock the XMLHttpRequest and WebSocket objects created by the Socket.IO client, wouldn't it? So that the responses from the server can be manually injected.

janeklb commented 1 year ago

I'm not quite following, but would like to understand what you mean.. Are you suggesting that in order to use jest.useFakeTimers(), then ...

... one would need to mock the XMLHttpRequest and WebSocket objects created by the Socket.IO client [?]

If so, why would that be necessary?

darrachequesne commented 1 year ago

@janeklb actually, I'm not sure to understand the issue here. Could you please provide a sample test case?

janeklb commented 1 year ago

@janeklb actually, I'm not sure to understand the issue here. Could you please provide a sample test case?

The issue is as described in the issue title/body: it's not possible to test socket.io code when using jest.useFakeTimers(); thats said, I'm not really invested in this issue anymore so I'm going to disengage 👋🏻