Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Promise.catch() does not catch exception in AngularJS unit test

I am writing Jasmine unit tests for my app in Typescript and running them via Resharper. It is supposed to execute an action if the handler throws an exception:

describe("Q Service Test", () => {
    var q: ng.IQService;
    var rootScope: ng.IRootScopeService;

    beforeEach(inject(($q, $rootScope) => {
        q = $q;
        rootScope = $rootScope;
    }));

    it("Caught exceptions are handled properly", () => {
        var state = 'ok';
        q.when(1)
            .then(() => {
                throw new Error("test exception");
            })
            .catch(() => {
                state = 'error';
            });

        rootScope.$digest();
        expect(state).toBe('error');
    });
});

However, the test is failed:

Resharper test fail

Is it some strange behaviour of my testing environment / tools, or am I incorrectly using the promise mechanism itself?

like image 328
Impworks Avatar asked Jul 21 '15 11:07

Impworks


2 Answers

You are definitely using the promise mechanism incorrectly, throwing a user-defined throw statement does not constitute to catching it as a promise rejection properly. As stated in the $q documentation:

When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of reject as the throw keyword in JavaScript. This also means that if you "catch" an error via a promise error callback and you want to forward the error to the promise derived from the current promise, you have to "rethrow" the error by returning a rejection constructed via reject.

They are similar but not equivalents, to catch user-defined throw statements, you should use catch statement blocks. While $q promises should only catch rejected promises. Thus, in your case, returning a rejected a promise is the proper way of handling the process instead of throwing a user-defined exception.

DEMO

JAVASCRIPT

describe('Q Service Test', function() {

  var $q,
      $rootScope;

  beforeEach(inject(function(_$q_, _$rootScope_) {
    $q = _$q_;
    $rootScope = _$rootScope_;
  }));

  it('Rejected promises are handled properly', function() {

    var state = 'ok';

    $q.when(1)
      .then(function() {
        return $q.reject('rejected');
      })
      .catch(function() {
        state = 'error';
      });

    $rootScope.$digest();
    expect(state).toBe('error');    

  });

});

UPDATE:

The reason why your code behaves this way in the browser, is because Angular's $q implementation uses the try/catch statement blocks in processing the promise queue. When any of the callbacks throw any errors, it catches the error itself, rejects it with the exception as a reason for rejection, afterwards it uses $exceptionHandler to log the error. I recommend you to simply return rejected promise.

As for the reason why the unit tests act this way is because of angular-mocks implementation of the $exceptionHandler is different with the actual application's $exceptionHandler. The former creates a provider with different modes, the default angular-mocks implementation uses the rethrow mode which in turn throws the exception instead of logging it. If you want to have your unit tests behave the same way as how the default application's $exceptionHandler then you can set the mode to 'log'.

DEMO

JAVASCRIPT

describe('Q Service Test', function() {

  var $q,
      $rootScope;

  beforeEach(module('ng', function($exceptionHandlerProvider) {
    $exceptionHandlerProvider.mode('log');
  }));

  beforeEach(inject(function(_$q_, _$rootScope_) {
    $q = _$q_;
    $rootScope = _$rootScope_;
  }));

  it('Caught exceptions are handled properly', function() {

    var state = 'ok';

    $q.when(1)
      .then(function() {
        throw new Error();
      })
      .catch(function() {
        state = 'error';
      });

    $rootScope.$digest();
    expect(state).toBe('error');    

  });

});
like image 133
ryeballar Avatar answered Nov 01 '22 06:11

ryeballar


The blog post Using and chaining promises in AngularJS mentions that when you throw and exception it "will also trigger Angular's registered exception handler". So my guess is that Jasmine is using Angular's exception handler to listening for exceptions.

Do you need to throw an exception or could you just do something like this:

return q.reject(new Error("test exception"));
like image 2
rob Avatar answered Nov 01 '22 05:11

rob