Ссылка на первую часть этой серии
Ссылка на вторую часть этой серии
Ссылка на третью часть этой серии
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, так как это последняя ветка промышленного выпуска. Если из-за этого возникнут какие-либо исправления / ошибки, я обновлю их в отдельном посте.