0

I am trying to test a bit of nodejs code that does the following (Connector is an eventEmitter) using jest

async () => {
  let initialisationComplete = false;
  while (!initialisationComplete) {
    await new Promise (accept => {
      const initialConnection = new Connector();
      initialConnection.once('connect',err => {
        if(err) {
          accept();
        } else {
          initialisationComplete = true;
          .... //go on to do some other async stuff before calling accept()
        }
      });
      initialConnection.connect();
    });
    if (!initialisationComplete) {
      await new Promise(accept => setTimeout(accept, 10000));
    }
  }
}

I have mocked the complete module to contains the connector and can essentially get an event emitted to me when a Connector.connect() is called, which enables me to then emit the "connect" event back.

I have told jest to use fakeTimers, and I want to test the error condition to show that in an error scenario, we get a ten second delay and then a retry

My test code is essentially this:-

           err = {
              toString: () => 'testing'
            }
            connection.emit('connect', err);
            console.log('before promise resolve')
            Promise.resolve().then(() => {
              jest.runAllTimers();
            }); //we have to allow for the promise to resolve before we can run the timers forward

The problem I have is that I am calling jest.runAllTimers before the test code has a chance to setup the 10 second timeout, so my test fails because the timeout never happens and the test itself times out.

My argument is that in the synchronous code that gets executed as a result of connection.emit('connect',err) causes the code under test to resolve the promise in the loop. I then call Promise.resolve().then(... I would have thought that the "accepted" Promise should already be on the promise processing queue when I call Promise.resolve() in the test, so its resolution should follow.

However lots of console.logs show the accept() gets called first, then Promise.resolve() in the test gets called, BUT, the then(() => jest.runAllTimers()) gets called next AND then the code after Promise in the while loop gets called (which then calls the setTimeout

Firstly I don't now why its like that rather than the Promise.resolve().then. Is it perhaps because the promise gets resolved and then the await has to be rescheduled?

Secondly - is there a way I can somehow delay (bearing in mind I cannot use timers)?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
akc42
  • 4,893
  • 5
  • 41
  • 60
  • Not an answer, but in the 1st code block, you are making a meal of somthing quite simple. What you are missing is [Promise.race()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race). Try `Promise.race([p1, p2])` where `p1` is the promisification of `initialConnection.once()`, and `p2` is the promisification of the `setTimeout()`. The `while` loop, the variable `initialisationComplete` and the need for `async/await` all disappear. – Roamer-1888 Nov 20 '21 at 18:31
  • Unfortunately, without knowing seeing more of the surrounding context it is very hard to reason about this problem. As I see it, the big missing pieces are: 1) where is the first function defined and approximately where it is called; 2) where is the connection variable initialised; and 3) can you clarify what jest.runAllTimers is actually doing? – David Huculak Nov 20 '21 at 18:37
  • As a side note, if you want to get a better understanding of the order of execution of promises in JavaScript, I highly recommend the presentation called "In The Loop" by Jake Archibald. It can be found on YouTube. – David Huculak Nov 20 '21 at 18:39
  • You will still need `await` (or `.then()`) but, with my suggestion, you can `await Promise.race([p1, p2])` followed by `go on to do some other async stuff` (pulled out of the promisification of `initialConnection.once()` but still dependent on the connection having been made). – Roamer-1888 Nov 20 '21 at 18:52
  • Be sure to `reject(err)`, on error, not `accept()`. – Roamer-1888 Nov 20 '21 at 18:52
  • @Roamer-1888 There is quite a lot to unpack in your comment, which I will do shortly to make sure I understand it, but thanks for the effort. Its a piece of code that is a bit of a mess and I have wanted to tidy up (although it does work on my dev machine when the database is slow in starting up in the morning). – akc42 Nov 21 '21 at 10:51
  • @DavidHuculak jest replaces `setTimeout` with its own version. `jest.runAllTimers` make sure at the time it is called that any timers that are pending are completed. So if I run it before my code does `setTimeout` that timeout never completes (jest has turned of the clock in `setTimeout`), I had actually seems Jake's YT video. but will look again. I did actually solve my problem by changing the `Promise.resolve().then` in my test to `new Prromise(accept => accept()).then` – akc42 Nov 21 '21 at 10:58
  • @Roamer-1888 - Now that I have examined in detail what you said, it is incorrect. I am not racing the promises, what I am doing is after it has failed (and only if its failed) I wait 10 seconds before trying again. However I have tidied it up, by using reject on error and catching the result in a `try` `catch` block and then doing the timeout in the `catch` block – akc42 Nov 21 '21 at 13:07
  • @akc42, sorry, my bad. I understand better now. Yes, reject and try/catch is the way to go if you stick with async/await. Given that you probably want to limit the number of connection attempts, you might consider [this approach](https://stackoverflow.com/a/38225011/3478010). See how compact (and readable) the code is with old fashioned `.then().catch()`. – Roamer-1888 Nov 21 '21 at 13:51
  • "*Is it perhaps because the promise gets resolved and then the await has to be rescheduled?*" - yes. `async`/`await` has subtly different scheduling than `.then()`, but you shouldn't concern yourself with the details. It would change if you'd add another `await` or chain another `.then()`, and you want your tests to run consistently. So far, the only approach to solve this problem is to use a macro task, see the `flushPromises` solution in the duplicate. – Bergi Nov 21 '21 at 19:06
  • @Bergi problem with `flushPromises` is it uses `setImmediate`. It is looking like that is disabled with `jest.useFakeTimers` (at least my tests timeout if I use it). I found a solution by using this `new Promise(async accept => { await new Promise(resolve => resolve()); accept(); }).then(() => { jest.runAllTimers(); }); //we have to allow for the promise to resolve before we can run the timers forward` – akc42 Nov 22 '21 at 18:02
  • Hm, that's weird, [it's supposed to work](https://stackoverflow.com/q/51643615/1048572). Or maybe [no longer](https://jestjs.io/blog/2020/05/05/jest-26#new-fake-timers)? Or [it's a bug](https://github.com/facebook/jest/issues/10221)? You might need to call `runAllTimers()` twice. I would avoid using that nested `new Promise` thingy - if really needed, write `await Promise.resolve().then().then().then();`, chain as many `then`s as you need to get it to work. – Bergi Nov 22 '21 at 21:04
  • You might be able to work around it by using the real `setImmediate`, not the mocked one - just capture a reference to it before calling `jest.useFakeTimers()`. See also [this](https://stackoverflow.com/q/51126786/1048572) and [that](https://stackoverflow.com/q/52177631/1048572) older question, which might be better duplicates. – Bergi Nov 22 '21 at 21:07

0 Answers0