Angularjs单元测试使用外部模板解决自定义指令内的承诺

时间:2015-01-23 06:55:01

标签: angularjs unit-testing angularjs-directive angular-promise angularjs-templates

我有一个使用外部模板的自定义指令,并从服务传递数据。我决定在修改数据之前确保承诺得到解决,这在实际代码中很好但是破坏了我的单元测试,这很烦人。我尝试了很多变化,但现在卡住了。我正在使用'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>

在主控制器中传递流派服务,即

 angular.module('magicApp')
.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre', 
function ($scope, BREAKPOINTS, Genre) {
    $scope.bp = BREAKPOINTS;
    $scope.genres = Genre.query();
}]);

2 个答案:

答案 0 :(得分:0)

好的,我会将您放入链接的代码移动到控制器中。数据处理可能应该在服务中进行。我知道你被告知大型控制器坏了,但是大的链接功能通常更糟糕,并且绝不应该进行那种数据处理。

.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;
            }
        });
    };
});

您的链接功能现在为空。相反,在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();
}));

应该在控制器测试中通过模拟服务的返回值来测试promise的行为。 $ promise测试的问题已经解决了。

实际问题是你假设$ q.defer()给你的角度为$ http的承诺,但这可以通过设计来解决。

答案 1 :(得分:0)

正如彼得所说,从指令中删除诺言并将其添加到控制器

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');
 });

}); 主控制器的单元测试现在具有所选的附加属性

'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}]);
    });
});

});