使用success()和error()测试控制器

nix*_*xon 22 javascript unit-testing jasmine angularjs

我正在尝试找出控制器中单元测试成功和错误回调的最佳方法.我能够模拟出服务方法,只要控制器只使用默认的$ q函数,例如'then'(参见下面的例子).当控制器响应"成功"或"错误"承诺时,我遇到了问题.(对不起,如果我的术语不正确).

这是一个示例controller\service

var myControllers = angular.module('myControllers');

myControllers.controller('SimpleController', ['$scope', 'myService',
  function ($scope, myService) {

      var id = 1;
      $scope.loadData = function () {
          myService.get(id).then(function (response) {
              $scope.data = response.data;
          });
      };

      $scope.loadData2 = function () {
          myService.get(id).success(function (response) {
              $scope.data = response.data;
          }).error(function(response) {
              $scope.error = 'ERROR';
          });
      }; 
  }]);


cocoApp.service('myService', [
    '$http', function($http) {
        function get(id) {
            return $http.get('/api/' + id);
        }
    }
]);  
Run Code Online (Sandbox Code Playgroud)

我有以下测试

'use strict';

describe('SimpleControllerTests', function () {

    var scope;
    var controller;
    var getResponse = { data: 'this is a mocked response' };

    beforeEach(angular.mock.module('myApp'));

    beforeEach(angular.mock.inject(function($q, $controller, $rootScope, $routeParams){

        scope = $rootScope;
        var myServiceMock = {
            get: function() {}
        };

        // setup a promise for the get
        var getDeferred = $q.defer();
        getDeferred.resolve(getResponse);
        spyOn(myServiceMock, 'get').andReturn(getDeferred.promise);

        controller = $controller('SimpleController', { $scope: scope, myService: myServiceMock });
    }));


    it('this tests works', function() {
        scope.loadData();
        expect(scope.data).toEqual(getResponse.data);
    });

    it('this doesnt work', function () {
        scope.loadData2();
        expect(scope.data).toEqual(getResponse.data);
    });
});
Run Code Online (Sandbox Code Playgroud)

第一个测试通过,第二个测试失败,出现错误"TypeError:Object不支持属性或方法'成功'".我在这个实例中得到了getDeferred.promise没有成功函数.好的,这是一个问题,编写此测试的好方法是什么,以便我可以测试模拟服务的"成功","错误"和"然后"条件?

我开始认为我应该避免在我的控制器中使用success()和error()......

编辑

所以在考虑了这个之后,并且由于下面的详细解答,我得出结论,处理控制器中的成功和错误回调是不好的.正如HackedByChinese在下面提到成功\错误是由$ http添加的语法糖.所以,实际上,通过尝试处理成功\错误我让$ http关注泄漏到我的控制器,这正是我试图通过在服务中包装$ http调用来避免.我要采取的方法是更改​​控制器不使用success\error:

myControllers.controller('SimpleController', ['$scope', 'myService',
  function ($scope, myService) {

      var id = 1;
      $scope.loadData = function () {
          myService.get(id).then(function (response) {
              $scope.data = response.data;
          }, function (response) {
              $scope.error = 'ERROR';
          });
      };
  }]);
Run Code Online (Sandbox Code Playgroud)

这样我就可以通过在延迟对象上调用resolve()和reject()来测试错误\成功条件:

'use strict';

describe('SimpleControllerTests', function () {

    var scope;
    var controller;
    var getResponse = { data: 'this is a mocked response' };
    var getDeferred;
    var myServiceMock;

    //mock Application to allow us to inject our own dependencies
    beforeEach(angular.mock.module('myApp'));
    //mock the controller for the same reason and include $rootScope and $controller
    beforeEach(angular.mock.inject(function($q, $controller, $rootScope, $routeParams) {

        scope = $rootScope;
        myServiceMock = {
            get: function() {}
        };
        // setup a promise for the get
        getDeferred = $q.defer();
        spyOn(myServiceMock, 'get').andReturn(getDeferred.promise);
        controller = $controller('SimpleController', { $scope: scope, myService: myServiceMock });  
    }));

    it('should set some data on the scope when successful', function () {
        getDeferred.resolve(getResponse);
        scope.loadData();
        scope.$apply();
        expect(myServiceMock.get).toHaveBeenCalled();
        expect(scope.data).toEqual(getResponse.data);
    });

    it('should do something else when unsuccessful', function () {
        getDeferred.reject(getResponse);
        scope.loadData();
        scope.$apply();
        expect(myServiceMock.get).toHaveBeenCalled();
        expect(scope.error).toEqual('ERROR');
    });
});
Run Code Online (Sandbox Code Playgroud)

Hac*_*ese 26

正如有人在删除的答案中提到的那样,success并且error添加了语法糖,$http所以当你创建自己的诺言时它们就不存在了.您有两种选择:

1 - 不要模拟服务并使用$httpBackend设置期望和刷新

我们的想法是让你的myService行为正常,而不知道它正在接受测试.$httpBackend将让您设置期望和响应,并刷新它们,以便您可以同步完成测试.$http不会更聪明,它返回的承诺看起来和功能就像一个真实的.如果您的HTTP期望很少,那么此选项很有用.

'use strict';

describe('SimpleControllerTests', function () {

    var scope;
    var expectedResponse = { name: 'this is a mocked response' };
    var $httpBackend, $controller;

    beforeEach(module('myApp'));

    beforeEach(inject(function(_$rootScope_, _$controller_, _$httpBackend_){ 
        // the underscores are a convention ng understands, just helps us differentiate parameters from variables
        $controller = _$controller_;
        $httpBackend = _$httpBackend_;
        scope = _$rootScope_;
    }));

    // makes sure all expected requests are made by the time the test ends
    afterEach(function() {
      $httpBackend.verifyNoOutstandingExpectation();
      $httpBackend.verifyNoOutstandingRequest();
    });

    describe('should load data successfully', function() {

        beforeEach(function() {
           $httpBackend.expectGET('/api/1').response(expectedResponse);
           $controller('SimpleController', { $scope: scope });

           // causes the http requests which will be issued by myService to be completed synchronously, and thus will process the fake response we defined above with the expectGET
           $httpBackend.flush();
        });

        it('using loadData()', function() {
          scope.loadData();
          expect(scope.data).toEqual(expectedResponse);
        });

        it('using loadData2()', function () {
          scope.loadData2();
          expect(scope.data).toEqual(expectedResponse);
        });
    });

    describe('should fail to load data', function() {
        beforeEach(function() {
           $httpBackend.expectGET('/api/1').response(500); // return 500 - Server Error
           $controller('SimpleController', { $scope: scope });
           $httpBackend.flush();
        });

        it('using loadData()', function() {
          scope.loadData();
          expect(scope.error).toEqual('ERROR');
        });

        it('using loadData2()', function () {
          scope.loadData2();
          expect(scope.error).toEqual('ERROR');
        });
    });           
});
Run Code Online (Sandbox Code Playgroud)

2 - 返回一个完全嘲弄的承诺

如果您正在测试的东西具有复杂的依赖性并且所有设置都很令人头疼,您可能仍然希望按照您的尝试来模拟服务和调用.不同之处在于你要完全嘲笑承诺.这可能是创建所有可能的模拟承诺的缺点,但是您可以通过创建自己的函数来创建这些对象,从而使这更容易.

这部作品的原因是因为我们假装它解决了通过调用所提供的处理程序success,errorthen立即使其同步完成.

'use strict';

describe('SimpleControllerTests', function () {

    var scope;
    var expectedResponse = { name: 'this is a mocked response' };
    var $controller, _mockMyService, _mockPromise = null;

    beforeEach(module('myApp'));

    beforeEach(inject(function(_$rootScope_, _$controller_){ 
        $controller = _$controller_;
        scope = _$rootScope_;

        _mockMyService = {
            get: function() {
               return _mockPromise;
            }
        };
    }));

    describe('should load data successfully', function() {

        beforeEach(function() {

          _mockPromise = {
             then: function(successFn) {
               successFn(expectedResponse);
             },
             success: function(fn) {
               fn(expectedResponse);
             }
          };

           $controller('SimpleController', { $scope: scope, myService: _mockMyService });
        });

        it('using loadData()', function() {
          scope.loadData();
          expect(scope.data).toEqual(expectedResponse);
        });

        it('using loadData2()', function () {
          scope.loadData2();
          expect(scope.data).toEqual(expectedResponse);
        });
    });

    describe('should fail to load data', function() {
        beforeEach(function() {
          _mockPromise = {
            then: function(successFn, errorFn) {
              errorFn();
            },
            error: function(fn) {
              fn();
            }
          };

          $controller('SimpleController', { $scope: scope, myService: _mockMyService });
        });

        it('using loadData()', function() {
          scope.loadData();
          expect(scope.error).toEqual("ERROR");
        });

        it('using loadData2()', function () {
          scope.loadData2();
          expect(scope.error).toEqual("ERROR");
        });
    });           
});
Run Code Online (Sandbox Code Playgroud)

即使在大型应用程序中,我也很少选择2.

对于它的价值,你的loadDataloadData2http处理程序有一个错误.它们引用response.data但是处理程序将直接使用已解析的响应数据进行调用,而不是响应对象(因此它应该data代替response.data).