Статьи

Я не издеваюсь над вами, только ваши AngularJS тесты

Вы также можете назвать этот пост «Избегайте избыточности в ваших модульных тестах 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);    });
});

Это так просто! Теперь я высмеиваю свои тесты вместо того, чтобы издеваться надо мной. Если у вас есть более сложный компонент для моделирования, вы можете использовать встроенные помощники. Например, Жасмин отправляет своих шпионов для этой цели.

Суть в том, чтобы избежать сложности (когда компонент вызывает компонент, вызывает компонент), а также изолировать ваши компоненты, чтобы у каждого из них была одна ответственность, а затем высмеивать ваши зависимости. Вы можете даже расти, чтобы испытывать больше удовольствия!

Что вы думаете о тестировании?