Angularjs 单元测试使用外部模板解析自定义指令内的承诺
Angularjs unit test resolve promise inside custom directive with external template
我有一个使用外部模板并从服务传递数据的自定义指令。我决定确保在修改数据之前解决了承诺,这在实际代码中很好但破坏了我的单元测试,这很烦人。我尝试了多种变体,但现在卡住了。我正在使用 'ng-html2js' 预处理器。
这是单元测试
describe('ccAccordion', function () {
var elm, scope, deferred, promise, things;
beforeEach(module('ccAccordion'));
// load the templates
beforeEach(module('components/accordion/accordion.html'));
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
things = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
deferred = $q.defer();
promise = deferred.promise;
promise.then(function (things) {
scope.items = things;
});
// Simulate resolving of promise
deferred.resolve(things);
// Propagate promise resolution to 'then' functions using $apply().
scope.$apply();
// compile the template?
$compile(elm)(scope);
scope.$digest();
}));
it('should create clickable titles', function () {
var titles = elm.find('.cc-accord h2');
expect(titles.length).toBe(2);
expect(titles.eq(0).text().trim()).toBe('Scifi');
expect(titles.eq(1).text().trim()).toBe('Comedy');
});
我省略了自定义 addMatchers 和其余测试。我得到的错误是
TypeError: 'undefined' is not an object (evaluating 'scope.items.$promise')
这是指令
var ccAccordion = angular.module("ccAccordion", []);
ccAccordion.directive("ccAccordion", function () {
return {
restrict: "AE",
templateUrl: "components/accordion/accordion.html",
scope: {
items: "="
},
link: function (scope) {
scope.items.$promise.then(function (items) {
angular.forEach(scope.items, function (item) {
item.selected = false;
});
items[0].selected = true;
});
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
}
};
});
这是main.html
中使用指令的地方
<cc-accordion items="genres"></cc-accordion>
在主控制器中传入流派服务ie
angular.module('magicApp')
.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre',
function ($scope, BREAKPOINTS, Genre) {
$scope.bp = BREAKPOINTS;
$scope.genres = Genre.query();
}]);
好的,我会将您在 link 中输入的代码移动到控制器中。数据处理可能应该发生在服务中。我知道有人告诉您大型控制器不好,但大型 linking 功能通常更差,永远不应该进行那种数据处理。
.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre',
function ($scope, BREAKPOINTS, Genre) {
$scope.bp = BREAKPOINTS;
$scope.genres = Genre.query().then(function (items) {
angular.forEach(scope.items, function (item) {
item.selected = false;
});
items[0].selected = true;
});
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
});
您的 link 函数现在是空的。相反,在 rootScope 上定义项目,这可确保 isolateScope 和您的指令接口正常工作。
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
things = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
scope.items = things; // Tests your directive interface
// compile the template?
$compile(elm)(scope);
scope.$digest();
}));
应在控制器测试中通过模拟服务的 return 值来测试承诺的行为。您的 $promise 测试问题已解决。
实际问题是您假设 $q.defer() 给了您与 angular $http 相同的承诺,但这是通过设计解决的。
正如彼得所说,从指令中删除承诺并将其添加到控制器
angular.module('magicApp')
.controller('MainCtrl', ['$scope', 'Genre',
function ($scope, Genre) {
$scope.genres = Genre.query();
$scope.genres.$promise.then(function () {
angular.forEach($scope.genres, function (genre) {
genre.selected = false;
});
$scope.genres[0].selected = true;
});
}]);
这也将允许控制器指定选择哪个选项卡开始。
在指令中
var ccAccordion = angular.module("ccAccordion", []);
ccAccordion.directive("ccAccordion", function () {
return {
restrict: "AE",
templateUrl: "components/accordion/accordion.html",
scope: {
items: "="
},
link: function (scope) {
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
}
};
});
指令单元测试现在看起来像这样
describe('ccAccordion', function () {
var elm, scope, deferred, promise, things;
beforeEach(module('ccAccordion'));
beforeEach(function () {
jasmine.addMatchers({
toHaveClass: function () {
return {
compare: function (actual, expected) {
var classTest = actual.hasClass(expected);
classTest ? classTest = true : classTest = false;
return {
pass: classTest,
message: 'Expected ' + angular.mock.dump(actual) + ' to have class ' + expected
};
}
};
}
});
});
// load the templates
beforeEach(module('components/accordion/accordion.html'));
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
scope.genres = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
$compile(elm)(scope);
scope.$digest();
}));
it('should create clickable titles', function () {
var titles = elm.find('.cc-accord h2');
expect(titles.length).toBe(2);
expect(titles.eq(0).text().trim()).toBe('Scifi');
expect(titles.eq(1).text().trim()).toBe('Comedy');
});
it('should bind the content', function () {
var contents = elm.find('.cc-accord-content div:first-child');
expect(contents.length).toBe(2);
expect(contents.eq(0).text().trim()).toBe('Scifi description');
expect(contents.eq(1).text().trim()).toBe('Comedy description');
});
it('should change active content when header clicked', function () {
var titles = elm.find('.cc-accord h2'),
divs = elm.find('.cc-accord');
// click the second header
titles.eq(1).find('a').click();
// second div should be active
expect(divs.eq(0)).not.toHaveClass('active');
expect(divs.eq(1)).toHaveClass('active');
});
});
并且主控制器的单元测试现在添加了 属性 of selected
'use-strict';
describe('magicApp controllers', function () {
// using addMatcher because $resource is not $http and returns a promise
beforeEach(function () {
jasmine.addMatchers({
toEqualData: function () {
return {
compare: function (actual, expected) {
return {
pass: angular.equals(actual, expected)
};
}
};
}
});
});
beforeEach(module('magicApp'));
beforeEach(module('magicServices'));
describe('MainCtrl', function () {
var scope, ctrl, $httpBackend;
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('/api/genres').
respond([{title: 'Scifi', selected: true}, {title: 'Comedy', selected: false}]);
scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {$scope: scope});
}));
it('should create "genres" model with 2 genres fetched from xhr', function () {
expect(scope.genres).toEqualData([]);
$httpBackend.flush();
expect(scope.genres).toEqualData(
[{title: 'Scifi', selected: true}, {title: 'Comedy', selected: false}]);
});
});
});
我有一个使用外部模板并从服务传递数据的自定义指令。我决定确保在修改数据之前解决了承诺,这在实际代码中很好但破坏了我的单元测试,这很烦人。我尝试了多种变体,但现在卡住了。我正在使用 'ng-html2js' 预处理器。
这是单元测试
describe('ccAccordion', function () {
var elm, scope, deferred, promise, things;
beforeEach(module('ccAccordion'));
// load the templates
beforeEach(module('components/accordion/accordion.html'));
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
things = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
deferred = $q.defer();
promise = deferred.promise;
promise.then(function (things) {
scope.items = things;
});
// Simulate resolving of promise
deferred.resolve(things);
// Propagate promise resolution to 'then' functions using $apply().
scope.$apply();
// compile the template?
$compile(elm)(scope);
scope.$digest();
}));
it('should create clickable titles', function () {
var titles = elm.find('.cc-accord h2');
expect(titles.length).toBe(2);
expect(titles.eq(0).text().trim()).toBe('Scifi');
expect(titles.eq(1).text().trim()).toBe('Comedy');
});
我省略了自定义 addMatchers 和其余测试。我得到的错误是
TypeError: 'undefined' is not an object (evaluating 'scope.items.$promise')
这是指令
var ccAccordion = angular.module("ccAccordion", []);
ccAccordion.directive("ccAccordion", function () {
return {
restrict: "AE",
templateUrl: "components/accordion/accordion.html",
scope: {
items: "="
},
link: function (scope) {
scope.items.$promise.then(function (items) {
angular.forEach(scope.items, function (item) {
item.selected = false;
});
items[0].selected = true;
});
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
}
};
});
这是main.html
中使用指令的地方<cc-accordion items="genres"></cc-accordion>
在主控制器中传入流派服务ie
angular.module('magicApp')
.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre',
function ($scope, BREAKPOINTS, Genre) {
$scope.bp = BREAKPOINTS;
$scope.genres = Genre.query();
}]);
好的,我会将您在 link 中输入的代码移动到控制器中。数据处理可能应该发生在服务中。我知道有人告诉您大型控制器不好,但大型 linking 功能通常更差,永远不应该进行那种数据处理。
.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre',
function ($scope, BREAKPOINTS, Genre) {
$scope.bp = BREAKPOINTS;
$scope.genres = Genre.query().then(function (items) {
angular.forEach(scope.items, function (item) {
item.selected = false;
});
items[0].selected = true;
});
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
});
您的 link 函数现在是空的。相反,在 rootScope 上定义项目,这可确保 isolateScope 和您的指令接口正常工作。
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
things = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
scope.items = things; // Tests your directive interface
// compile the template?
$compile(elm)(scope);
scope.$digest();
}));
应在控制器测试中通过模拟服务的 return 值来测试承诺的行为。您的 $promise 测试问题已解决。
实际问题是您假设 $q.defer() 给了您与 angular $http 相同的承诺,但这是通过设计解决的。
正如彼得所说,从指令中删除承诺并将其添加到控制器
angular.module('magicApp')
.controller('MainCtrl', ['$scope', 'Genre',
function ($scope, Genre) {
$scope.genres = Genre.query();
$scope.genres.$promise.then(function () {
angular.forEach($scope.genres, function (genre) {
genre.selected = false;
});
$scope.genres[0].selected = true;
});
}]);
这也将允许控制器指定选择哪个选项卡开始。
在指令中
var ccAccordion = angular.module("ccAccordion", []);
ccAccordion.directive("ccAccordion", function () {
return {
restrict: "AE",
templateUrl: "components/accordion/accordion.html",
scope: {
items: "="
},
link: function (scope) {
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
}
};
}); 指令单元测试现在看起来像这样
describe('ccAccordion', function () {
var elm, scope, deferred, promise, things;
beforeEach(module('ccAccordion'));
beforeEach(function () {
jasmine.addMatchers({
toHaveClass: function () {
return {
compare: function (actual, expected) {
var classTest = actual.hasClass(expected);
classTest ? classTest = true : classTest = false;
return {
pass: classTest,
message: 'Expected ' + angular.mock.dump(actual) + ' to have class ' + expected
};
}
};
}
});
});
// load the templates
beforeEach(module('components/accordion/accordion.html'));
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
scope.genres = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
$compile(elm)(scope);
scope.$digest();
}));
it('should create clickable titles', function () {
var titles = elm.find('.cc-accord h2');
expect(titles.length).toBe(2);
expect(titles.eq(0).text().trim()).toBe('Scifi');
expect(titles.eq(1).text().trim()).toBe('Comedy');
});
it('should bind the content', function () {
var contents = elm.find('.cc-accord-content div:first-child');
expect(contents.length).toBe(2);
expect(contents.eq(0).text().trim()).toBe('Scifi description');
expect(contents.eq(1).text().trim()).toBe('Comedy description');
});
it('should change active content when header clicked', function () {
var titles = elm.find('.cc-accord h2'),
divs = elm.find('.cc-accord');
// click the second header
titles.eq(1).find('a').click();
// second div should be active
expect(divs.eq(0)).not.toHaveClass('active');
expect(divs.eq(1)).toHaveClass('active');
});
}); 并且主控制器的单元测试现在添加了 属性 of selected
'use-strict';
describe('magicApp controllers', function () {
// using addMatcher because $resource is not $http and returns a promise
beforeEach(function () {
jasmine.addMatchers({
toEqualData: function () {
return {
compare: function (actual, expected) {
return {
pass: angular.equals(actual, expected)
};
}
};
}
});
});
beforeEach(module('magicApp'));
beforeEach(module('magicServices'));
describe('MainCtrl', function () {
var scope, ctrl, $httpBackend;
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('/api/genres').
respond([{title: 'Scifi', selected: true}, {title: 'Comedy', selected: false}]);
scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {$scope: scope});
}));
it('should create "genres" model with 2 genres fetched from xhr', function () {
expect(scope.genres).toEqualData([]);
$httpBackend.flush();
expect(scope.genres).toEqualData(
[{title: 'Scifi', selected: true}, {title: 'Comedy', selected: false}]);
});
});
});