如何使用ui-router测试依赖于服务的AngularJS控制器?

时间:2014-05-30 14:47:25

标签: javascript angularjs unit-testing karma-runner

我的测试是:

(function() {
  describe('Login Controller', function() {
    beforeEach(module('myApp'));
    beforeEach(inject(function($injector) {
      var $controller, $httpBackend, $q, userServiceMock;
      self.$rootScope = $injector.get('$rootScope');
      self.$scope = $rootScope.$new();
      self.$state = $injector.get('$state');
      $q = $injector.get('$q');
      $httpBackend = $injector.get('$httpBackend');
      $controller = $injector.get('$controller');
      userServiceMock = {
        login: function(auth) {
          self.deferred = $q.defer();
          console.log("HERE!");
          return self.deferred.promise;
        }
      };
      self.createController = function() {
        return $controller('LoginController', {
          '$scope': self.$scope,
          '$rootScope': self.$rootScope,
          '$state': self.$state,
          'userService': userServiceMock
        });
      };
      return $httpBackend.whenPOST('http://localhost:9001/api/v1/session/check').respond({
        authenticated: true
      });
    }));
    it('should set the page title to "Login"', function() {
      self.createController();
      $scope.init();
      expect($rootScope.pageTitle).toBe('Login');
      return expect($scope.auth).toEqual({});
    });
    return it('should properly authenticate a user', function() {
      self.createController();
      $scope.init();
      $scope.auth = {
        username: 'test@test.com',
        password: 'mypassword'
      };
      $scope.login();
      self.deferred.resolve({
        authenticated: true
      });
      $scope.$root.$digest();
      console.log($state.current.name);
      expect($state.current.name).toBe('dashboard');
    });
  });
})();

我认为很简单?我的控制器是:

myApp.controller('LoginController', [
  '$scope', '$rootScope', '$state', 'userService', function($scope, $rootScope, $state, userService) {
    $scope.init = function() {
      $rootScope.pageTitle = 'Login';
      return $scope.auth = {};
    };
    $scope.login = function() {
      return userService.login($scope.auth).then(function(response) {
        if (response.authenticated === true) {
          return $state.transitionTo('dashboard');
        } else {
          return console.log('bad password man');
        }
      });
    };
    return $scope.init();
  }
]);

我在测试中得到的是:

NFO [karma]: Karma v0.12.16 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.7 (Mac OS X)]: Connected on socket TRjrAWL8PHdQ-bKqkyzK with id 61166122
LOG: 'HERE!'
PhantomJS 1.9.7 (Mac OS X) - Login Controller:
PhantomJS 1.9.7 (Mac OS X)  should set the page title to "Login" PASSED
PhantomJS 1.9.7 (Mac OS X)  should properly authenticate a user FAILED
PhantomJS 1.9.7 (Mac OS X): Executed 2 of 2 (1 FAILED) (0.02 secs / 0.018 secs)
PhantomJS 1.9.7 (Mac OS X)
PhantomJS 1.9.7 (Mac OS X) Login Controller should properly authenticate a user FAILED
    Error: Unexpected request: GET templates/dashboard.html
    No more request expected

发生这种情况的原因是状态转换为dashboard,然后尝试加载dashboard.html模板。我不一定想要/需要测试它。我只需要测试它是否达到了正确的状态。如何正确设置?

我使用Angular UI Router代替内置$routeProvider

2 个答案:

答案 0 :(得分:6)

简短的回答是你需要告诉$ httpBackend来期待这个请求。如果任何请求被发送并且没有被$ httpBackend捕获,Karma将抛出错误,因为它们都应该在单元测试中被模拟。它可能看起来像......

$httpBackend.expectGET('/templates/dashboard.html');
$scope.login();
$httpBackend.flush();

我说这只是简短的答案,因为这里有一些事情,如果不提他们就会有点不利。首先,您使用$ httpBackend来捕获身份验证POST请求,但是您实际上并未发送请求,因为您正在模拟userService。你不需要同时做这两件事。您可以删除此代码......

return $httpBackend.whenPOST('http://localhost:9001/api/v1/session/check').respond({
    authenticated: true
});

因为该请求实际上不会被您的模拟用户服务发送。该代码可能在您的userService的单元测试中很有用。

我还会使用beforeEach / afterEach挂钩。当你可以在beforeEach函数中调用一次并且它将在每次测试之前运行时,不需要在所有测试中调用self.createController。如果你不小心,这可能会变得混乱。

最后,测试视图更改并设置新页面标题正在推动单元测试的限制。理想情况下,您可以在集成测试中测试路由和页面更改(如果有的话)。你在第二次测试中唯一真正测试的是......

$state.transitionTo('dashboard')

做了它应该做的事情(将当前的$状态更新为'仪表板')。你应该相信这是有效的。 UI路由器有自己的测试证明这是有效的。我会努力使您的单元测试尽可能小,并且仅隔离测试应用程序各个组件的特定行为。所以不要问......

"当我调用$ scope.login()时,是否会调用userService的登录函数并发送POST请求并更改状态?"

我只想问......

"当我调用$ scope.login()时,是否会调用userService的登录函数?"

其余部分不是您的控制器的责任。是否发送请求是userService的责任(并且应该在那里进行测试)以及当你调用$ state.transitionTo时状态是否发生变化是ui-router的范围并且已经在那里进行了测试。保留跨越多个组件的高级测试以进行集成测试。

答案 1 :(得分:4)

您可以模拟模板文件,从而避免$templateCache

出现HTTP错误
$templateCache = $injector.get('$templateCache');
$templateCache.put('templates/dashboard.html','<div>blank or whatever</div>');

......另一种方法是使用$httpBackend ...


顺便说一下,您可以随时将注射剂添加到$injector功能中,而不是一直使用inject ......

beforeEach(inject(function($rootScope, $state, $httpBackend, $templateCache) {

我经常使用下划线语法来获得额外的灵活性:

beforeEach(inject(function(_$rootScope_, _$state_, _$httpBackend_, _$templateCache_) {