Promise.catch() не исключает исключения в AngularJS unit test

Я пишу тесты Jasmine для моего приложения в Typescript и запускаю их через Resharper. Предполагается выполнить действие, если обработчик выдает исключение:

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');
    });
});

Однако тест не выполнен:

Resharper test fail

Это странное поведение моей тестовой среды/инструментов, или я неправильно использую сам механизм обещаний?

Ответ 1

Вы определенно используете механизм обещаний неправильно, бросая определяемый пользователем оператор throw, не означает, что он будет ловить его как отказ от обещаний. Как указано в документации $q:

При сравнении отложенных / promises с привычным поведением try/catch/throw, подумайте отклонить как ключевое слово throw в JavaScript. Это также означает, что если вы "поймаете" ошибку с помощью ошибки обещания обратный вызов, и вы хотите переслать ошибку на обещание, полученное из текущее обещание, вы должны "перебросить" ошибку, вернув отклонение, построенное с помощью отклонения.

Они похожи, но не эквивалентны, чтобы поймать пользовательские инструкции throw, вы должны использовать блоки операторов catch. Пока $q promises должен отклонить catch promises. Таким образом, в вашем случае возврат отклоненного обещания - это правильный способ обработки процесса, а не выбрасывание пользовательского исключения.

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:

Причина, по которой ваш код ведет себя таким образом в браузере, заключается в том, что реализация Angular $q использует блоки операторов try/catch при обработке очереди обещаний. Когда какой-либо из обратных вызовов вызывает какие-либо ошибки, он сам обнаруживает ошибку, отклоняет ее с исключением в качестве причины для отклонения, а затем использует $exceptionHandler для регистрации ошибки. Я рекомендую вам просто вернуть отклоненное обещание.

В связи с тем, почему модульные тесты действуют таким образом, из-за angular-mocks реализация $exceptionHandler отличается от фактическое приложение $exceptionHandler. Первый создает поставщика с различными режимами, реализация по умолчанию angular -mocks использует режим rethrow, который, в свою очередь, генерирует исключение вместо его регистрации. Если вы хотите, чтобы ваши модульные тесты ведут себя так же, как и приложение по умолчанию $exceptionHandler, вы можете установить режим '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');    

  });

});

Ответ 2

Сообщение в блоге Использование и привязка promises в AngularJS упоминает, что когда вы выбрасываете и исключаете, оно также вызывает Angular зарегистрированное исключение обработчик". Поэтому я предполагаю, что Jasmine использует обработчик исключений Angular для прослушивания исключений.

Вам нужно выбросить исключение или вы могли бы просто сделать что-то вроде этого:

return q.reject(new Error("test exception"));