Статьи

Модульное тестирование контроллеров AngularJS, представлений и многого другого: часть четвертая

Ссылка на первую часть этой серии

Ссылка на вторую часть этой серии

Ссылка на третью часть этой серии

AngularJS-RequireJS-Seed, используемый для справки, здесь .

Утка-Угловая здесь .

Этот пост содержит множество разных тем, чтобы глубже понять Duck-Angular, а также некоторые нюансы / подводные камни модульного тестирования в AngularJS.

Глупый HTTP, как вы хотите

Да, вы можете издеваться над $ httpBackend . Тем не менее, в большинстве случаев вы можете уйти, просто издеваясь над $ http . Вот пример из service1-test.js , который показывает вам, как это сделать. Основная причина использования API-интерфейсов success () и fail () для $ http заключается в том, что эти методы предоставляют обертки над обычным предложением then () с одним аргументом для расширения параметров до привычного (data, status, headers, config). формат. Нам не нужно самим распаковывать эту информацию. Таким образом, для непосредственного макетирования $ http эти функции должны быть имитированы.

Также обратите внимание, что эти обработчики ошибок могут быть объединены в цепочку, так что каждая ложная функция ( success () и fail () ) должна возвращать обещание.

define(["service1", "Q"], function (Service1Ctor, Q) {
  describe("Service1", function () {
    it("should work, even if the HTTP call succeeds", function (){
      var httpResponse = {successful: false};
      var promise = Q.all(httpResponse);
      promise.success = function(onSuccess) {
        promise.then(function(data) { return onSuccess(data); });
        return promise;
      };
      promise.error = function(onError) {
        promise.fail(function(err) { return onError(err); });
        return promise;
      };
      var getStub = sinon.stub().returns(promise);
      var httpMock = {get: getStub};
      var service1 = new Service1Ctor(httpMock, Q);
      var run = service1.getHttp("http://google.com");
      return expect(run).to.be.fulfilled;
    });
  });
});

Немного многословно, но это можно абстрагировать в функцию фиктивного строителя. Я нахожу, что проще использовать этот подход, когда я макетирую зависимость от $ http, вместо того, чтобы регистрировать модули Angular и проходить весь процесс начальной загрузки.

Почему $ q не всегда работает

Вот простой тестовый модуль из controller1-view-test.js . В этом модульном тесте нет ничего особенного: все, что он делает, это возвращает предварительно разрешенное обещание.

it("can prove that $q won't work in a plain unit test", function () {
  return mother.createMvc("route2Controller", "../templates/route2.html", {}).then(function (mvc) {
    var injector = mvc.injector;
    var $q = injector.get("$q");
    var d = $q.defer();
    d.resolve({});
    return d.promise;
  });
});

Когда вы запустите этот тест, вы получите сообщение об ошибке таймаута Mocha, например:

Error: timeout of 10000ms exceeded
    at http://localhost/example/static/js/test/lib/mocha/mocha.js?bust=1388996292503:3993:14

Причина использования $ q вместо Q в простом модульном тесте не работает, потому что механика * $ q тесно связана с жизненным циклом области видимости AngularJS. Действительно, если вы посмотрите на код для $ q (внутри QProvider ), вы увидите вызов $ evalAsync () , который не дает никаких гарантий относительно того, когда будет вызван обратный вызов, за исключением следующего:

  • Он будет выполняться в текущем контексте выполнения скрипта (перед любым рендерингом DOM).
  • По крайней мере один цикл $ digest будет expressionвыполнен.

Простым решением для всех этих ситуаций является использование Q вместо $ q . Если вы должны выполнить $ Q зависимость для службы или контроллера в простом тестовом модуле (тот , который не бутстраповские AngularJS модулей), нагнетающий Q .

Разрешение дочерних шаблонов

Шаблон может иметь несколько партиалов, каждый из которых может содержать дополнительные вложенные партиалы. Обычно, когда вы используете шаблоны модульного тестирования, эта загрузка / привязка вложенных частичек полностью обрабатывается AngularJS. Но есть предостережение: мы не знаем, когда все частичные файлы были загружены.

В более старых версиях Duck-Angular попытка модульного тестирования шаблонов с вложенными частями иногда приводила к ошибкам, поскольку тест начался до того, как AngularJS разрешил и связал все вложенные партиалы. В большинстве случаев расширение этих партиалов не произойдет, и любые утверждения об элементах DOM, которые, как ожидается, будут находиться внутри этих партиалов, потерпят неудачу.

В настоящее время Duck-Angular исправляет это, используя обширные механизмы публикации событий, которые предоставляет AngularJS. Событием, на котором мы сосредоточимся в этом обсуждении, является событие $ includeContentLoaded . Это событие публикуется при загрузке части с помощью директивы ng-include . Как это поможет нам? Что ж, если мы знаем, сколько партиалов существует в шаблоне (независимо от уровня вложенности), мы можем установить счетчик, который будет тиковаться каждый раз, когда происходит событие $ includeContentLoaded , и начинать наш тест только тогда, когда счетчик достигнет числа Частицы, которые мы обнаружили.

Это звучит очень хорошо в теории, за исключением одной крошечной складки: нам нужно фактически загрузить все партиалы, чтобы на самом деле выяснить, сколько их существует, для начала. Это именно то, что делает Duck-Angular. Это создает цепочку обещаний. Каждое обещание отвечает за загрузку отдельного частичного. Как только этот фрагмент загружен, это обещание объединяется с другими обещаниями, которые, в свою очередь, загружают дочерние частичные объекты. Результат выполнения каждого обещания — один плюс ценность выполнения обещаний ребенка.

Это просто рекурсивная цепочка обещаний, каждое из которых объединяет счетчики своих дочерних обещаний и передает их в дерево. Результатом является общее количество появившихся обещаний, которое имеет однозначное соответствие с количеством загруженных частичек. Код ниже из duck-angular.js иллюстрирует это.

  var includes = element.find("[ng-include]");
  if (includes.length === 0) {
    return Q.fcall(function () {
      return 1;
    });
  }

  var promises = _.map(includes, function (include) {
    var includeSource = angular.element(include).attr("src").replace("'", "").replace("'", "");
    var includePromise = requireQ(["text!" + includeSource]);
    return includePromise.spread(function (sourceText) {
      var child = self.removeElementsBelongingToDifferentScope(self.createElement(sourceText));
      return num(child);
    });
  });
  return Q.all(promises).then(function (counts) {
    return 1 + _.reduce(counts, function (sum, count) {
      return sum + count;
    }, 0);
  });
};

Подводные камни модульного тестирования

  • Если вы используете Mocha-as-Promised, обязательно всегда явно возвращайте обещание. Если вы этого не сделаете, тест, скорее всего, пройдет успешно (потому что у Mocha нет способа решить, когда тест закончится), но, вероятно, потерпел бы неудачу.

  • Хорошей практикой при использовании Chai-as-Promised является использование предложений should.be.fulfilled () и should.be.rejected () вместо использования простого then () . Если ваше обещание не выполнено, любые утверждения / шаги в предложении then () никогда не будут выполнены, и ваш тест будет успешно выполнен. Использование явной проверки выполнения / отказа Chai-as-Promised предотвращает это.

Этот (вид) завершает эту серию модульным тестированием контроллеров и представлений AngularJS с использованием Duck-Angular. Я, вероятно, обновлю код для использования AngularJS 1.2.x, так как это последняя ветка промышленного выпуска. Если из-за этого возникнут какие-либо исправления / ошибки, я обновлю их в отдельном посте.