4

I am testing a function that returns a promise. I want to assert that, in certain conditions, the returned promise would never settle (doesn't resolve nor reject).

How can I test this with Mocha?


If I run the following:

describe('under certain conditions', function () {
  let promise;
  beforeEach(function () {
    promise = new Promise((resolve, reject) => {});
  });
  it('should hang forever', function () {
    return promise;
  });
});

I get the following error:

Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves
dayuloli
  • 16,205
  • 16
  • 71
  • 126
  • 1
    Define "never". – robertklep Sep 11 '18 at 19:52
  • As in the example given above - `new Promise((resolve, reject) => {})` would never settle right? – dayuloli Sep 11 '18 at 19:57
  • You can't really. The only way to ensure that the promise won't ever be resolved is proofing that there are no `resolve`/`reject` functions alive that still reference it. So something like `gc.collect(); const a = gc.memory(); promise = null; gc.collect(); const b = gc.memory(); return a - b < 0` could possibly do it, but it doesn't look reliable. – Bergi Sep 11 '18 at 19:57
  • @dayuloli did you wait an infinite amount of time to make sure? – robertklep Sep 11 '18 at 19:57
  • @robertklep right. I get your question now. I _do_ want to wait a very long time to make sure. I am using the [`lolex`](https://github.com/sinonjs/lolex) library for that. But even after 'fake wait'-ing 100 seconds, the `it` block would still throw that error. – dayuloli Sep 11 '18 at 20:00
  • @dayuloli you can increase the default 2s waiting time that Mocha uses: `mocha -t 1324512000000 ...` – robertklep Sep 11 '18 at 20:01
  • @robertklep But that means my test will hang forever (which is not what I want). I was thinking about using a `try`/`catch` block to catch the Mocha error, but it doesn't seem to work. – dayuloli Sep 11 '18 at 20:03
  • @Bergi Where does `gc` come from? Do I need to install a library for that? – dayuloli Sep 11 '18 at 20:05
  • 1
    @dayuloli See https://stackoverflow.com/a/30654451/1048572 (I think I got the syntax wrong, and I also doubt that it works at all, but you can try) – Bergi Sep 11 '18 at 20:18

4 Answers4

3

Let's start by stating that practically speaking, it's not possible to validate that the promise never settles: at some point you have to decide that it has taken too long for the promise to settle, and assume that it will never settle after that point.

Here's a solution that would place that point at 5 seconds:

it('should hang forever', function() {
  // Disable Mocha timeout for this test.
  this.timeout(0);

  // Wait for either a timeout, or the promise-under-test to settle. If the
  // promise that settles first is not the timeout, fail the test.
  return Promise.race([
    new Promise(resolve => setTimeout(resolve, 5000, 'timeout')),
    promise.then(
      () => { throw Error('unexpectedly resolved') },
      () => { throw Error('unexpectedly rejected') }
    )
  ]);
});
robertklep
  • 198,204
  • 35
  • 394
  • 381
  • 1
    You might want to do `Promise.race([delay(…), promise.then(() => { throw … }, () => { throw … })])` instead of handling resolution afterwards and comparing for the `"timeout"` value – Bergi Sep 11 '18 at 20:38
3

robertklep's answer works, but you'd have to wait for 5 seconds before the test completes. For unit tests, 5 seconds is simply too long.

As you've suggested, you can integrate the lolex library into robertklep's solution, to avoid the wait.

(I am also using a Symbol instead of the string 'timeout', in case your promise resolves, by coincidence, also resolves with the string 'timeout')

import { install } from 'lolex';

describe('A promise', function () {
  let clock;
  before(function () { clock = install() });
  after(function () { clock.uninstall() });

  describe('under certain conditions', function () {
    const resolvedIndicator = Symbol('resolvedIndicator');
    const forever = 600000; // Defining 'forever' as 10 minutes
    let promise;
    beforeEach(function () {
      promise = Promise.race([
        new Promise(() => {}), // Hanging promise
        new Promise(resolve => setTimeout(resolve, forever, resolvedIndicator)),
      ]);
    });
    it('should hang forever', function () {
      clock.tick(forever);
      return promise.then((val) => {
        if (val !== resolvedIndicator) {
          throw Error('Promise should not have resolved');
        }
      }, () => {
        throw Error('Promise should not have rejected');
      });
    });
  });
});
d4nyll
  • 11,811
  • 6
  • 54
  • 68
  • 1
    +1 for using `Symbol`. As for using `lolex`, wouldn't that only be useful if the _actual_ use case depends on some sort of timeout? I assume that OP doesn't really want to test `new Promise(() => {})` never settling, but some promise inside their app. Also, it may interfere with Mocha's own timekeeping. – robertklep Sep 12 '18 at 07:32
  • 1
    When you run `lolex.install()`, it'll override the global instance of `setTimeout`, at that time, it stores the current time with their internal epoch (starts at `0`). When you run `clock.tick()`, it simply forwards that epoch by that amount and checks whether any callbacks needs to execute. So maybe `lolex` does interfere with Mocha's timekeeping, since it would override the global `setTimeout`, but in this case, I don't think it matters, since it won't timeout, and we restore the original behavior after the tests. (You can use an inner `beforeEach` / `afterEach` to have more granularity) – d4nyll Sep 12 '18 at 14:15
  • (At least that's how I'd imagine `lolex` would work, I haven't looked at the source code) – d4nyll Sep 12 '18 at 14:16
1

Try this:

describe('under certain conditions', function () {

    let promise;
    beforeEach(function () {
        promise = new Promise((resolve, reject) => {
        // promise.reject();
      });
    });

    it('should hang forever', function (done) {
        const onRejectOrResolve = () => {
            done(new Error('test was supposed to hang'));
        };
        promise
        .then(onRejectOrResolve)
        .catch(onRejectOrResolve);
        setTimeout(() => {
            done();
        }, 1000);
    });

  });
ImGroot
  • 796
  • 1
  • 6
  • 17
  • Be it any tool/library (not just Mocha). You'll have to limit "the forever" in test representations i.e. how long you want to wait before marking your test case passed/failed. Since mocha has a default timeout of 2000 ms, I have put your wait limit to 1000ms here. You can always adjust mocha test timeout but make sure your wait time should be less than that. – ImGroot Sep 11 '18 at 20:28
1

You can introduce a race between the never resolving promise and a reference promise with a suitable timeout using Promise.race (MDN):

const p1 = new Promise((resolve, reject) => { });

const p2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 5 * 1000, 'promise2');
});


Promise.race([p1, p2])
.then(value => { 
  console.log(value); 
});
FK82
  • 4,907
  • 4
  • 29
  • 42