Единичное тестирование в AngularJS - Mocking Services и Promises
В Angular все, кажется, имеет крутую кривую обучения, а модульное тестирование приложения Angular определенно не ускользает от этой парадигмы.
Когда я начал работу с TDD и Angular, я чувствовал, что тратил дважды (возможно, больше) столько времени, сколько выяснил, как тестировать и, возможно, даже более точно настроить мои тесты правильно. Но как Ben Nadel помещал его в свой блог, в процессе обучения Angular происходят взлеты и падения. Его график, безусловно, мой опыт работы с Angular.
Однако по мере того, как я прогрессировал в процессе обучения Angular и модульного тестирования, теперь я чувствую, что я трачу гораздо меньше времени на установление тестов и гораздо больше времени, чтобы тесты проходили от красного до зеленого - что является хорошим чувством.
Итак, я столкнулся с различными методами настройки unit test для издевательства сервисов и promises, и я подумал, что поделился бы тем, что узнал, а также задал вопрос:
Есть ли какие-либо другие или лучшие способы для этого?
Таким образом, на код, что все мы все пришли сюда, чтобы не слушать кого-то, кто говорит о своей любви, совершает ошибки, изучая рамки.
Вот как я начал издеваться над моими услугами и promises, я буду использовать контроллер, но службы и promises могут быть издевательскими в других местах, очевидно.
describe('Controller: Products', function () {
var//iable declarations
$scope,
$rootScope,
ProductsMock = {
getProducts: function () {
} // There might be other methods as well but I'll stick to one for the sake of consiseness
},
PRODUCTS = [{},{},{}]
;
beforeEach(function () {
module('App.Controllers.Products');
});
beforeEach(inject(function ($controller, _$rootScope_) {
//Set up our mocked promise
var promise = { then: jasmine.createSpy() };
//Set up our scope
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
//Set up our spies
spyOn(ProductsMock, 'getProducts').andReturn(promise);
//Initialize the controller
$controller('ProductsController', {
$scope: $scope,
Products: ProductsMock
});
//Resolve the promise
promise.then.mostRecentCall.args[0](PRODUCTS);
}));
describe('Some Functionality', function () {
it('should do some stuff', function () {
expect('Stuff to happen');
});
});
});
Для нас это сработало, но со временем я подумал, что должен быть лучший способ. Для одного я ненавидел
promise.then.mostRecentCall
и если бы мы хотели повторно инициализировать контроллер, нам пришлось вытащить его из блока beforeEach и вставить его в каждый тест.
Должен быть лучший способ...
Теперь я спрашиваю, есть ли у кого-нибудь другие способы тестирования тестов или мысли или чувства по тому, как я решил это сделать?
Ответы
Ответ 1
Затем я наткнулся на другой пост, блог, пример stackoverflow (вы выбрали его, я, вероятно, там), и я увидел использование библиотеки $q. Duh! Зачем устанавливать целое обещание, когда мы можем просто использовать инструмент, который Angular дает нам. Наш код выглядит красивее и имеет смысл смотреть - не уродливое обещание.
Далее в итерации модульного тестирования было следующее:
describe('Controller: Products', function () {
var//iable declarations
$scope,
$rootScope,
$q,
$controller,
productService,
PROMISE = {
resolve: true,
reject: false
},
PRODUCTS = [{},{},{}] //constant for the products that are returned by the service
;
beforeEach(function () {
module('App.Controllers.Products');
module('App.Services.Products');
});
beforeEach(inject(function (_$controller_, _$rootScope_, _$q_, _products_) {
$rootScope = _$rootScope_;
$q = _$q_;
$controller = _$controller_;
productService = _products_;
$scope = $rootScope.$new();
}));
function setupController(product, resolve) {
//Need a function so we can setup different instances of the controller
var getProducts = $q.defer();
//Set up our spies
spyOn(products, 'getProducts').andReturn(getProducts.promise);
//Initialise the controller
$controller('ProductsController', {
$scope: $scope,
products: productService
});
// Use $scope.$apply() to get the promise to resolve on nextTick().
// Angular only resolves promises following a digest cycle,
// so we manually fire one off to get the promise to resolve.
if(resolve) {
$scope.$apply(function() {
getProducts.resolve();
});
} else {
$scope.$apply(function() {
getProducts.reject();
});
}
}
describe('Resolving and Rejecting the Promise', function () {
it('should return the first PRODUCT when the promise is resolved', function () {
setupController(PRODUCTS[0], PROMISE.resolve); // Set up our controller to return the first product and resolve the promise.
expect('to return the first PRODUCT when the promise is resolved');
});
it('should return nothing when the promise is rejected', function () {
setupController(PRODUCTS[0], PROMISE.reject); // Set up our controller to return first product, but not to resolve the promise.
expect('to return nothing when the promise is rejected');
});
});
});
Это начало чувствовать, что он должен быть настроен. Мы можем издеваться над тем, что нам нужно, чтобы издеваться над нами, мы можем установить наше обещание разрешить и отклонить, чтобы мы могли действительно проверить два возможных результата. Это чувствует себя хорошо...
Ответ 2
Главное в вашем собственном ответе об использовании $q.defer
звучит хорошо. Мое единственное дополнение -
setupController(0, true)
не является особенно ясным из-за параметров 0
и true
, а затем оператора if
, который использует это. Кроме того, передача макета products
в функцию $controller
сама по себе кажется необычной, и означает, что у вас может быть 2 различных products
доступных сервиса. Один из них непосредственно вводится в контроллер и один вводится обычной системой Angular DI в другие службы. Я думаю, что лучше использовать $provide
для ввода mocks, а затем всюду в Angular будет иметь тот же самый экземпляр для любого теста.
Объединяя все это, кажется, что-то вроде следующего, что можно увидеть на http://plnkr.co/edit/p676TYnAIb9QlD7MPIHu?p=preview
describe('Controller: ProductsController', function() {
var PRODUCTS, productsMock, $rootScope, $controller, $q;
beforeEach(module('plunker'));
beforeEach(module(function($provide){
PRODUCTS = [{},{},{}];
productsMock = {};
$provide.value('products', productsMock);
}));
beforeEach(inject(function (_$controller_, _$rootScope_, _$q_, _products_) {
$rootScope = _$rootScope_;
$q = _$q_;
$controller = _$controller_;
products = _products_;
}));
var createController = function() {
return $controller('ProductsController', {
$scope: $rootScope
})
};
describe('on init', function() {
var getProductsDeferred;
var resolve = function(results) {
getProductsDeferred.resolve(results);
$rootScope.$apply();
}
var reject = function(reason) {
getProductsDeferred.reject(reason);
$rootScope.$apply();
}
beforeEach(function() {
getProductsDeferred = $q.defer();
productsMock.getProducts = function() {
return getProductsDeferred.promise;
};
createController();
});
it('should set success to be true if resolved with product', function() {
resolve(PRODUCTS[0]);
expect($rootScope.success).toBe(true);
});
it('should set success to be false if rejected', function() {
reject();
expect($rootScope.success).toBe(false);
});
});
});
Обратите внимание, что отсутствие инструкции if
и ограничение объекта getProductsDeferred
и getProducts
mock в объем блока describe
. Используя этот тип шаблона, вы можете добавлять другие тесты по другим методам products
, не загрязняя объект mock products
или функцию setupController
, которая у вас есть, со всеми возможными методами/комбинациями, которые вам нужны для тесты.
Как боковая панель, я замечаю:
module('App.Controllers.Products');
module('App.Services.Products');
означает, что вы разделяете свои контроллеры и службы на разные модули Angular. Я знаю, что некоторые блоги рекомендовали это, но я подозреваю, что это сложнейшие вещи, и один модуль для каждого приложения в порядке. Если вы затем реорганизуете и сделаете сервисы и директивы полностью разделяющими повторно используемые компоненты, тогда пришло время поместить их в отдельный модуль и использовать их, как и любой другой сторонний модуль.
Изменить: Исправлено $provide.provide
до $provide.value
и исправлено некоторое упорядочение создания контроллера/служб и добавлена ссылка на Plunkr