Вы также можете назвать этот пост «Избегайте избыточности в ваших модульных тестах Angular».
Существует множество подходов к реализации сервисных вызовов в Angular. Некоторые разработчики используют службу $ resource для чистых конечных точек REST. Я предпочитаю изолировать веб-взаимодействия в отдельном компоненте, который возвращает обещание. Хотя $ http возвращает обещание, вы должны знать, как анализировать фрагменты, например, смотреть на свойство данных вместо самого результата, поэтому я предпочитаю обернуть его.
Сервис в моих приложениях выглядит примерно так:
var blogExample;
(function (blogExample) {
"use strict";
var app = blogModule.getModule();
function service(baseUrl, $http, $q) { this.url = baseUrl + "api/examples"; this.$q = $q; this.$http = $http; }
angular.extend(service.prototype, {
getStuff: function() { var defer = this.$q.defer(); this.$http.get(this.url).then(function(result) { defer.resolve(result.data); }, function(err) { defer.reject(err); }); return defer.promise; } });
app.service("blogExampleSvc", ["baseUrl", "$http", "$q", service]);
})(blogExample || (blogExample = {}));
Если вы используете TypeScript, вы можете добавить ему интерфейс, например так:
module BlogExample { interface IExampleService { getStuff: () => ng.IPromise<any>; }
}
Теперь, если вы когда-либо работали с Angular, вы можете писать тесты, подключив ngMock и настроив службу $ httpBackend . Я мог бы проверить, что служба правильно отвечает на успешный запрос, как этот (с использованием Jasmine ):
(function () { "use strict";
var exampleService, httpBackend;
describe("exampleService", function () {
beforeEach(function () {
module("blogExample", function ($provide) { $provide.constant("baseUrl", http://unittest/); }); });
beforeEach(inject(function ($httpBackend, blogExampleSvc) { httpBackend = $httpBackend; exampleService = blogExampleSvc; }));
afterEach(function () { httpBackend.verifyNoOutstandingExpectation(); httpBackend.verifyNoOutstandingRequest(); });
it("is registered with the module.", function () { expect(exampleService).not.toBeNull(); });
it("should set the proper URL for the service", function() { expect(exampleService.url).toBe(http://unittest/api/examples); });
describe("getStuff",function () { it("should return stuff upon successful call", function () { exampleService.getStuff() .then(function (result) { expect(result).not.toBeNull(); }, function () { expect(false).toBe(true); }); httpBackend.expectGET(exampleService.url).respond(200, []); httpBackend.flush(); }); }); });
})();
(Вы захотите проверить больше, чем просто нуль на вызове, но я пытаюсь сделать это простым.)
Неизбежно, что сервис будет втянут в контроллер. По иронии судьбы, это когда динамическая природа JavaScript облегчает тестирование, но я все еще вижу людей, конфигурирующих бэкэнд HTTP (test, mock) для своих тестов контроллера! Зачем?
Помните, что модульные тесты должны быть простыми, быстрыми и легко написанными. Когда вы тестируете сервис, вы тестируете, что сервис выполняет свою работу. Он должен совершать звонки, которые ему нужны, и обрабатывать коды ответов. Когда вы тестируете контроллер, вы должны предполагать, что служба прошла его тестирование. Вы должны заботиться только о том, как контроллер взаимодействует со службой, а не о том, как служба взаимодействует с серверной частью. Это избыточно!
Поэтому для того, чтобы иметь дело с контроллером, который зависит от вашего сервиса, вы должны насмехаться над сервисом.
Есть несколько способов сделать это. Самое простое — просто создать свой собственный макет на лету. JavaScript действительно хорош в этом. Вот пример, где я создаю макет напрямую и использую его для подсчета вызовов. Обратите внимание, что в конфигурации модуля я переопределяю «реальное» определение сервиса своим фиктивным, а затем использую инжектор, чтобы подключить сервис обещаний. Я также определяю удобную функцию с именем flushPromises, которая запускает дайджест, чтобы обещания были выполнены.
(function() { "use strict";
var controllerSvc, exampleController, exampleSvcMock, flushPromises;
describe("exampleCtrl", function() {
beforeEach(function() {
exampleSvcMock = { count: 0, $q: null, getStuff: function() { var defer = this.$q.defer(); defer.resolve([]); this.count += 1; return defer.promise; } }
module("blogExample", function($provide) { $provide.constant("baseUrl", "http://unittest/"); $provide.value("blogExampleSvc", exampleSvcMock); }); });
beforeEach(inject(function($controller, $q, $rootScope) { exampleSvcMock.$q = $q; controllerSvc = $controller; flushPromises = function() { $rootScope.$apply(); }; })); });
});
Если бы я использовал TypeScript, я бы определил мой макет как тип IExampleService, чтобы убедиться, что я удовлетворяю любым требованиям контракта. Теперь вместо того, чтобы устанавливать ожидания бэкэнда, я просто запрашиваю мой макет:
describe("refresh", function () { it("calls getStuff", function () { var count; exampleController = controllerSvc("exampleCtrl"); count = exampleSvcMock.count; exampleController.refresh(); flushPromises(); expect(exampleSvcMock.count).toBeGreaterThan(count); });
});
Это так просто! Теперь я высмеиваю свои тесты вместо того, чтобы издеваться надо мной. Если у вас есть более сложный компонент для моделирования, вы можете использовать встроенные помощники. Например, Жасмин отправляет своих шпионов для этой цели.
Суть в том, чтобы избежать сложности (когда компонент вызывает компонент, вызывает компонент), а также изолировать ваши компоненты, чтобы у каждого из них была одна ответственность, а затем высмеивать ваши зависимости. Вы можете даже расти, чтобы испытывать больше удовольствия!
Что вы думаете о тестировании?