【问题标题】:How to test services in an AngularJS Controller?如何在 AngularJS 控制器中测试服务?
【发布时间】:2014-05-07 15:19:44
【问题描述】:

我的控制器是:

angularMoonApp.controller('SourceController', ['$scope', '$rootScope', '$routeParams', 'fileService', function ($scope, $rootScope, $routeParams, fileService) {
  $scope.init = function() {
    $rootScope.currentItem = 'source';

    fileService.getContents($routeParams.path).then(function(response) {
      $scope.contents = response.data;
      $scope.fileContents = null;
      if(_.isArray($scope.contents)) {
        // We have a listing of files
        $scope.breadcrumbPath = response.data[0].path.split('/'); 
      } else {
        // We have one file
        $scope.breadcrumbPath = response.data.path.split('/');
        $scope.breadcrumbPath.push('');

        $scope.fileContents = atob(response.data.content);

        fileService.getCommits(response.data.path).then(function(response) {
          $scope.commits = response.data;
        });
      }
    });
  }

  $scope.init();
}]);

我的测试很简单:

(function() {
  describe('SourceController', function() {
    var $scope, $rootScope, $httpBackend, createController;

    beforeEach(module('angularMoon'));

    beforeEach(inject(function($injector) {
      $httpBackend = $injector.get('$httpBackend');
      $rootScope = $injector.get('$rootScope');
      $scope = $rootScope.$new();

      var $controller = $injector.get('$controller');

      createController = function() {
        return $controller('SourceController', {
          '$scope': $scope
        });
      };
    }));

    it("should set the current menu item to 'source'", function() {
      createController();
      $scope.init();
      expect($rootScope.currentItem).toBe('source');
    });

    it("should get the contents of the root folder", function() {
      createController();
      $scope.init();

      // NOT SURE WHAT TO DO HERE!
    });

  });
})();

我想测试 fileService 是否调用了 getContents 函数并模拟响应,以便我可以测试这两种情况(如果是数组,如果不是)

【问题讨论】:

    标签: angularjs unit-testing karma-jasmine


    【解决方案1】:

    我建议为此使用 Jasmine 间谍。

    这是一个可能有帮助的例子。我通常将 spyOn 调用放在 beforeEach 中。

    var mockedResponse = {};
    spyOn(fileService, "getContents").andReturn(mockedResponse);
    

    在“它”部分:

    expect(fileService.getContents).toHaveBeenCalled();
    

    要获得响应,只需调用控制器中调用 fileService 方法的方法。您可能还需要手动运行摘要循环。我的一项测试的片段:

        var testOrgs = [];
        beforeEach(inject(function(coresvc) {
            deferred.resolve(testOrgs);
            spyOn(coresvc, 'getOrganizations').andReturn(deferred.promise);
    
            scope.getAllOrganizations();
            scope.$digest();
        }));
    
        it("getOrganizations() test the spy call", inject(function(coresvc) {
            expect(coresvc.getOrganizations).toHaveBeenCalled();
        }));
    
        it("$scope.organizations should be populated", function() {
            expect(scope.allOrganizations).toEqual(testOrgs);
            expect(scope.allOrganizations.length).toEqual(0);
        });
    

    在这种情况下,deferred 是使用 $q.defer(); 创建的承诺;

    【讨论】:

    • 您能否添加一个更完整的 $q 示例以及您如何使用它?
    • 另外,如果你想测试 2 个不同的响应怎么办?
    • 我们的服务向控制器返回承诺,所以我不得不模拟响应,就好像它是一个承诺一样。从字面上看,唯一缺少的行是 var deferred = $q.defer();当我想测试不同的模拟输出时,我添加了第二个具有不同模拟响应的测试。有时我会调用 deferred.resolve(someMock);控制 promise 的输出。
    • 所以如果我想为不同的测试提供不同的数据,我不应该把间谍放在beforeEach,对吗?相反,我应该在测试中这样做?
    • 你可以有嵌套/多个 beforeEach。每个“测试”(至少我是这样做的)都有一个 beforeEach 和一些它的。
    【解决方案2】:

    您可以创建一个间谍并仅验证 fileService.getContents 是否被调用,或者通过使间谍调用通过来验证额外的调用(如承诺解析)。可能您还应该与 httpBackend 交互,因为您可能需要刷新 http 服务(即使您使用模拟服务)。

    (function() {
      describe('SourceController', function() {
        var $scope, $rootScope, $httpBackend, createController, fileService;
    
        beforeEach(module('angularMoon'));
    
        beforeEach(inject(function($injector) {
          $httpBackend = $injector.get('$httpBackend');
          $rootScope = $injector.get('$rootScope');
          $scope = $rootScope.$new();
    
          // See here
          fileService = $injector.get('fileService');
          spyOn(fileService, 'getContents').andCallThrough();
    
          var $controller = $injector.get('$controller');
    
          createController = function() {
            return $controller('SourceController', {
              '$scope': $scope
              'fileService': fileService
            });
          };
        }));
    
        it("should get the contents of the root folder", function() {
          createController();
          $scope.init();
    
          expect(fileService.getContents).toHaveBeenCalled();
        });
    
      });
    })();
    

    您还可以对回调中发生的事情添加预期,但您应该在之前发出httpBackend.flush()

    【讨论】:

    • 调用意味着你现在正在运行真正的服务代码。这可能不是 OP 想要的。通常在单元测试中,您会希望模拟响应并避免调用实际的服务。
    • 如何模拟响应?所以有时我希望getContents 返回数据 X 有时返回数据 Y
    • @aet 这就是为什么说你可以这样做,而不是你应该这样做。通常您只想知道该方法已被调用,并且没有进一步的交互问题。
    • @Shamoon 我认为 aet 已经用 andReturn 方法回答了您的问题。
    • 不是我,@aet 的回答 :)
    猜你喜欢
    • 1970-01-01
    • 2013-03-09
    • 1970-01-01
    • 2012-05-20
    • 2013-06-02
    • 1970-01-01
    • 2023-04-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多