socket.io icon indicating copy to clipboard operation
socket.io copied to clipboard

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

Open MiniSuperDev opened this issue 1 year ago • 10 comments

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:

  • Node: v16.13.1
  • OS: Windows 10

MiniSuperDev avatar Nov 28 '22 10:11 MiniSuperDev

+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).

janeklb avatar Feb 22 '23 16:02 janeklb

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 avatar Feb 23 '23 15:02 darrachequesne

Closed due to inactivity, please reopen if needed.

darrachequesne avatar Jun 20 '23 15:06 darrachequesne

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

aarowman avatar Aug 31 '23 19:08 aarowman

@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.

darrachequesne avatar Sep 04 '23 20:09 darrachequesne

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().

janeklb avatar Sep 04 '23 21:09 janeklb

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.

darrachequesne avatar Sep 05 '23 07:09 darrachequesne

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?

janeklb avatar Sep 05 '23 22:09 janeklb

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

darrachequesne avatar Sep 13 '23 09:09 darrachequesne

@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 👋🏻

janeklb avatar Sep 16 '23 17:09 janeklb