单元测试视图 - 最佳实践

30

有人能分享一下关于单元测试视图的经验吗?我读了很多有关如何对视图进行单元测试的教程,但每种方法都有一些缺点。

我采用了以下方法。它有效,但我想知道是否有更好的方法来做这件事。同时也存在一些缺点,稍后我会解释。我还使用protractor进行端到端测试,但它们总是很慢,因此我将其限制在最小范围内。

这是我的控制器。它绑定了两个变量到它的$scope中,并在视图中使用:

// test_ctrl.js
angular.module('app', [])
  .controller('TestCtrl', ["$rootScope", "$scope", function ($rootScope, $scope) {
    $scope.bar = "TEST";
    $scope.jobs = [
      {name: "cook"}
    ];
  }]);

这个视图将 $scope.bar 放入一个 <span> 中,并将 $scope.jobs 数组传递给 ng-repeat 指令:

<!-- test.html the view for this controller -->
<span>
  Bar is {{bar || "NOT SET"}}
</span>
<ul>
  <li ng-repeat="job in jobs">{{job.name}}</li>
</ul>

这是一个测试:

describe('Controller: TestCtrl', function () {
  beforeEach(module('templates'));
  beforeEach(module('app'));

  var TestCtrl, $rootScope, $compile, createController, view, $scope;
  beforeEach(inject(function($controller, $templateCache, _$rootScope_, _$compile_, _$httpBackend_) {
    $rootScope = _$rootScope_;
    $scope = $rootScope.$new();
    $compile = _$compile_;

    createController = function() {
      var html = $templateCache.get('views/test.html');
      TestCtrl = $controller('TestCtrl', { $scope: $scope, $rootScope: $rootScope });
      view = $compile(angular.element(html))($scope);
      $scope.$digest();
    };
  }));

  it('should test the view', function() {
    createController();
    expect(view.find("li").length).toEqual(1)
    console.log($scope.jobs)
  });
});
beforeEach函数中,我将设置控制器。从测试本身调用的createController函数会从$templateCache中获取视图,使用其自己的$scope创建控制器,然后编译模板并触发$digest
模板缓存使用Karma的预处理器ng-html2js进行预填充。
// karma.conf.js
...
preprocessors: {
  'app/views/*.html': 'ng-html2js'
}
...

采用这种方法,我遇到了一个小问题和一些疑问:

1. 在我的对象中,由于 ng-repeat ,出现了额外的 $$hashKey 键.

在我的测试中,expect($scope.jobs).toEqual([{name: "cook"}]); 抛出了一个错误:

Expected [ { name : 'cook', $$hashKey : '009' } ] to equal [ { name : 'cook' } ]

我知道 ng-repeat 会添加这些键,但这种测试很愚蠢。我唯一想到的解决方法是分离控制器测试和视图测试。但是当我在控制器中检查 jobs 数组时,$$hashKey 并不存在。有任何想法,为什么会出现这种情况?

2. $scope 问题

第一次尝试时,我只定义了本地作用域为 $scope={} 而不是像我在其他控制器测试中所做的那样定义 $scope = $rootScope.$new()。但是只使用一个普通对象作为本地作用域,我无法编译它($compile(angular.element(html))($scope); 抛出了一个错误)。

我还想知道将 $rootScope 本身作为当前控制器的本地作用域是否是个好主意,这是一个好的方法吗?或者是否还有我没有看到的任何缺点?

3. 最佳实践

我非常想知道每个人在 AngularJS 中如何进行单元测试。我认为视图必须经过测试,因为使用所有 Angular 指令会涉及许多逻辑,我希望看到这些逻辑被验证。


值得注意的是,在ng-repeat指令中使用_track by_表达式可能有助于摆脱哈希键(并加快查找速度)。请参见http://www.codelord.net/2014/04/15/improving-ng-repeat-performance-with-track-by/。 - titusn
2
这与单元测试无关,但在查看示例控制器后,我想留下此评论以供过路程序员学习:不要污染$scope与控制器的本地变量。最好使用this.bar = "TEST",并在视图中使用ng-controller="TestCtrl as ctrl"ctrl.bar进行引用。 - jrharshath
1个回答

25

我认为你所做的是一种很好的视图单元测试方法。你提出的代码对于想要进行视图单元测试的人来说是一个很好的示例。


1. ng-repeat $$hashKey

不要担心数据,相反,测试各种操作的结果,因为这才是最终你真正关心的。因此,使用jasmine-jquery验证创建控制器后DOM的状态以及模拟 click() 等操作后DOM的状态等。


2. $scope = $rootScope.$new() 没有问题

$rootScopeScope 的一个实例,而 $rootScope.$new() 创建了一个 ChildScope 的实例。使用 ChildScope 的实例进行测试技术上更正确,因为在实际生产中,控制器作用域(scope)也是 ChildScope 实例。

顺便说一下,对于创建隔离(scope)作用域的指令进行单元测试同样适用。当你使用 ChildScope 的实例来编译指令时,会自动创建一个隔离(scope)作用域(它是 Scope 的实例)。你可以使用 element.isolateScope() 访问该隔离(scope)作用域。

// decalare these variable here so we have access to them inside our tests
var element, $scope, isolateScope;

beforeEach(inject(function($rootScope, $compile) {
  var html = '<div my-directive></div>';

  // this scope is an instance of ChildScope
  $scope = $rootScope.$new();

  element = angular.element(html);   
  $compile(element)($scope);
  $scope.$digest();

  // this scope is an instance of Scope
  isolateScope = element.isolateScope(); 
}));

3. +1测试视图

有些人认为应该使用Protractor来测试视图。Protractor非常适用于想要测试整个堆栈(从前端到后端)的情况。但是,Protractor速度较慢,而单元测试速度快。这就是为什么使用单元测试来测试你的视图和指令会更加合理,可以通过模拟依赖于后端的任何应用程序部分来实现。

指令非常适合进行单元测试,控制器则不太适合。控制器可能有很多移动部件,这可能使它们更难以测试。因此,我赞成经常创建指令。结果是更具模块化的代码,更容易进行测试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接