简体   繁体   中英

How to write a test using Mocha+Chai to expect an exception from setTimeout?

I have following:

it('invalid use', () => {
  Matcher(1).case(1, () => {});
});

The case method is supposed to throw after some delay, how can I describe it for Mocha/Chai that's what I want - the test should pass (and must not pass when exception is not thrown)?

Consider case method off limits, it cannot be changed.

For testing purposes it should be equivalent to:

it('setTimeout throw', _ => {
  setTimeout(() => { throw new Error(); }, 1); // this is given, cannot be modified
});

I tried:

it('invalid use', done => {
  Matcher(1).case(1, () => {});
  // calls done callback after 'case' may throw
  setTimeout(() => done(), MatcherConfig.execCheckTimeout + 10);
});

But that's not really helping me, because the test behavior is exactly reverted - when an exception from case ( setTimeout ) is not thrown, it passes (should fail) and when an exception is thrown the test fails (should succeed).

I read somewhere someone mentioning global error handler, but I would like to solve this cleanly using Mocha and/or Chai, if it is possible (I guess Mocha is already using it in some way).

You cannot handle exceptions from within a asynchronous callback, eg see Handle error from setTimeout . This has to do with the execution model ECMAScript uses. I suppose the only way to catch it is in fact to employ some environment-specific global error handling, eg process.on('uncaughtException', ...) in Node.js.

If you convert your function to Promises, however, you can easily test it using the Chai plugin chai-as-promsied :

import * as chai from 'chai';

import chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const expect = chai.expect;

it('invalid use', async () => {
  await expect(Matcher(1).case(1, () => {})).to.eventually.be.rejected;
});

Any Mocha statements like before , after or it will work asynchronously if you return a promise. I generally use something like the below for async tests. Also don't forget to set timeout this.timeout(...) if you expect the async function to take more than 2 seconds.

it('some test', () => {
    return new Promise(function(resolve,reject){
        SomeAsyncFunction(function(error,vals) {
            if(error) {
                 return reject(error);    
            } else {
                try {
                    //do some chai tests here
                } catch(e) {
                    return reject(e);
                }
                return resolve();
            }
        });
    });
});

Specifically for your case, since we expect some error to be thrown after a period of time (assuming the empty callback you have provided to .case should not run due to the exception being thrown) then you can write it something like:

it('invalid use', () => {
    //define the promise to run the async function
    let prom = new Promise(function(resolve,reject){
        //reject the promise if the function does not throw an error
        //note I am assuming that the callback won't run if the error is thrown
        //also note this error will be passed to prom.catch so need to do some test to make sure it's not the error you are looking for.
        Matcher(1).case(1, () => {return reject(new Error('did not throw'))});
    });
    prom.catch(function(err){
        try {
            expect(err).to.be.an('error');
            expect(err.message).to.not.equal('did not throw');
            //more checks to see if err is the error you are looking for
        } catch(e) {
            //err was not the error you were looking for
            return Promise.reject(e);
        }
        //tests passed
        return Promise.resolve();
    });
    //since it() receives a promise as a return value it will pass or fail the test based on the promise.
    return prom;
});

If your tested code calls setTimeout with a callback that throws and no-one is catching this is exception then:

1) this code is broken

2) the only way to see that problem is platform global exception handler like process.on('uncaughtException' mentioned by user ComFreek

The last resort chance is to stub setTimeout for duration of test (for example using sinon.stub ) or just manually.

In such stubbed setTimeout you can decorate timeout handler, detect exception and call appropriate asserts.

NOTE, this is last resort solution - your app code is broken and should be fixed to properly propagate errors, not only for testing but ... well, to be good code.

Pseudocode example:

 it('test', (done) => { const originalSetTimeout = setTimeout; setTimeout = (callback, timeout) => { originalSetTimeout(() => { try { callback(); } catch(error) { // CONGRATS, you've intercepted exception // in _SOME_ setTimeout handler } }, timeout) } yourTestCodeThatTriggersErrorInSomeSetTimeoutCallback(done); }) 

NOTE2: I intentionally didn't wrote proper async cleanup code, it's a homework. Again, see sinon.js and its sandbox

NOTE3: It will catch all setTimeout calls during test duration. Beware, there are dragons.

From Chai documentation :

When no arguments are provided, .throw invokes the target function and asserts that an error is thrown.

So you could something like

expect(Matcher(1).case(1, () => {})).to.throw

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM