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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/23731637/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-28 01:28:56  来源:igfitidea点击:

Test a controller with success() and error ()

javascriptangularjsunit-testingjasmine

提问by nixon

I'm trying to work out the best way to unit test success and error callbacks in controllers. I am able to mock out service methods, as long as the controller only uses the default $q functions such as 'then' (see the example below). I'm having an issue when the controller responds to a 'success' or 'error' promise. (Sorry if my terminology is not correct).

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

Here is an example 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);
        }
    }
]);  

I have the following test

我有以下测试

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

The first test passes and the second fails with the error "TypeError: Object doesn't support property or method 'success'". I get that in this instance that getDeferred.promise does not have a success function. Okay here is the question, what is a nice way to write this test so that I can test the 'success', 'error' & 'then' conditions of a mocked service ?

第一个测试通过,第二个测试失败,并显示错误“TypeError: Object does not support property or method 'success'”。我知道在这个例子中 getDeferred.promise 没有成功功能。好的,问题是,编写此测试的好方法是什么,以便我可以测试模拟服务的“成功”、“错误”和“然后”条件?

I'm starting to think that I should avoid the use of success() and error() in my controllers...

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

EDIT

编辑

So after thinking about this some more, and thanks to the detailed answer below, I've come to the conclusion that the handling the success and error callbacks in the controller is bad.As HackedByChinese mentions below success\error is syntactic sugar that is added by $http. So, in actual fact, by trying to handle success \ error I am letting $http concerns leak into my controller, which is exactly what I was trying to avoid by wrapping the $http calls in a service. The approach I'm going to take is to change the controller not to use success \ error:

因此,在对此进行了更多思考之后,并且由于下面的详细答案,我得出的结论是,在控制器中处理成功和错误回调很糟糕。正如 HackedByChinese 在下面提到的,success\error 是由 $http 添加的语法糖。所以,实际上,通过尝试处理成功\错误,我让 $http 问题泄漏到我的控制器中,这正是我试图通过将 $http 调用包装在服务中来避免的。我要采取的方法是改变控制器不使用成功\错误:

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

This way I can test the error \ success conditions by calling resolve() and reject() on the deferred object:

这样我就可以通过在延迟对象上调用 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');
    });
});

回答by moribvndvs

As someone had mentioned in a deleted answer, successand errorare syntactic sugar added by $httpso they aren't there when you create your own promise. You have two options:

正如有人在已删除的答案中提到的那样,success并且error添加了语法糖,$http因此当您创建自己的承诺时它们不存在。您有两个选择:

1 - Don't mock the service and use $httpBackendto setup expectations and flush

1 - 不要嘲笑服务并用于$httpBackend设置期望和刷新

The idea is to let your myServiceact like it normally would without knowing it's being tested. $httpBackendwill let you set up expectations and responses, and flush them so you can complete your tests synchronously. $httpwon't be any wiser and the promise it returns will look and function like a real one. This option is good if you have simple tests with few HTTP expectations.

这个想法是让你的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');
        });
    });           
});

2 - Return a fully-mocked promise

2 - 返回一个完全模拟的承诺

If the thing you're testing has complicated dependencies and all the set-up is a headache, you may still want to mock the services and the calls themselves as you have attempted. The difference is that you'll want to fully mock promise. The downside of this can be creating all the possible mock promises, however you could make that easier by creating your own function for creating these objects.

如果您正在测试的东西具有复杂的依赖关系,并且所有设置都令人头疼,那么您可能仍然希望在尝试时模拟服务和调用本身。不同之处在于您需要完全模拟承诺。这样做的缺点是创建所有可能的模拟承诺,但是您可以通过创建自己的函数来创建这些对象,从而使这变得更容易。

The reason this works is because we pretend that it resolves by invoking the handlers provided by success, error, or thenimmediately, causing it to complete synchronously.

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

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

I rarely go for option 2, even in big applications.

我很少选择选项 2,即使在大型应用程序中也是如此。

For what it's worth, your loadDataand loadData2http handlers have an error. They reference response.databut the handlerswill be called with the parsed response data directly, not the response object (so it should be datainstead of response.data).

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

回答by Cesar Alvarado

Don't mix concerns!

不要混合担忧!

Using $httpBackendinside a controller is a bad Idea since you are mixing concerns inside your Test. Whether you retrieve data from an Endpoint or not is not a concern of the Controller, is a concern of the DataService you are calling.

使用$httpBackend一个控制器内是一个糟糕的主意,因为你混合你的测试里面的担忧。您是否从 Endpoint 检索数据不是 Controller 的问题,而是您正在调用的 DataService 的问题。

You can see this more clearly if you change the Endpoint Url inside the service you will then have to modify both tests: the service Test and the Controller Test.

如果您更改服务内的端点 URL,您可以更清楚地看到这一点,您将不得不修改两个测试:服务测试和控制器测试。

Also as previously mentioned, the use of successand errorare syntactic sugar and we should stick to the use of thenand catch. But in reality you may find yourself in the need of testing "legacy" code. So for that I'm using this function:

同样如前所述,successand的使用error是语法糖,我们应该坚持使用thenand catch。但实际上,您可能会发现自己需要测试“遗留”代码。因此,我正在使用此功能:

function generatePromiseMock(resolve, reject) {
    var promise;
    if(resolve) {
        promise = q.when({data: resolve});
    } else if (reject){
        promise = q.reject({data: reject});
    } else {
        throw new Error('You need to provide an argument');
    }
    promise.success = function(fn){
        return q.when(fn(resolve));
    };
    promise.error = function(fn) {
        return q.when(fn(reject));
    };
    return promise;
}

By calling this function you will get a true promise that respond to thenand catchmethods when you need to and will also work for the successor errorcallbacks. Note that the success and error returns a promise itself so it will work with chained thenmethods.

通过调用此函数,您将获得一个真正的承诺,该承诺在您需要时响应thencatch方法,并且也适用于successerror回调。请注意,成功和错误本身返回一个承诺,因此它将与链式then方法一起使用。

(NOTE: On the 4th and 6th line the function returns resolve and reject values inside the data property of an object. This is to mock the Behavior of $http since it returns the data, http Status etc.)

(注意:在第 4 行和第 6 行,该函数返回对象数据属性内的解析和拒绝值。这是为了模拟 $http 的行为,因为它返回数据、http 状态等。)

回答by Mohammed Ramadan

Yes, do not use $httpbackend in your controller, because we don't need to make real requests, you just need to make sure that one unit is doing it's job exactly as expected, have a look on this simple controller tests, it's easy to understand

是的,不要在你的控制器中使用 $httpbackend,因为我们不需要发出真正的请求,你只需要确保一个单元完全按预期完成工作,看看这个简单的控制器测试,这很容易了解

/**
 * @description Tests for adminEmployeeCtrl controller
 */
(function () {

    "use strict";

    describe('Controller: adminEmployeeCtrl ', function () {

        /* jshint -W109 */
        var $q, $scope, $controller;
        var empService;
        var errorResponse = 'Not found';


        var employeesResponse = [
            {id:1,name:'mohammed' },
            {id:2,name:'ramadan' }
        ];

        beforeEach(module(
            'loadRequiredModules'
        ));

        beforeEach(inject(function (_$q_,
                                    _$controller_,
                                    _$rootScope_,
                                    _empService_) {
            $q = _$q_;
            $controller = _$controller_;
            $scope = _$rootScope_.$new();
            empService = _empService_;
        }));

        function successSpies(){

            spyOn(empService, 'findEmployee').and.callFake(function () {
                var deferred = $q.defer();
                deferred.resolve(employeesResponse);
                return deferred.promise;
                // shortcut can be one line
                // return $q.resolve(employeesResponse);
            });
        }

        function rejectedSpies(){
            spyOn(empService, 'findEmployee').and.callFake(function () {
                var deferred = $q.defer();
                deferred.reject(errorResponse);
                return deferred.promise;
                // shortcut can be one line
                // return $q.reject(errorResponse);
            });
        }

        function initController(){

            $controller('adminEmployeeCtrl', {
                $scope: $scope,
                empService: empService
            });
        }


        describe('Success controller initialization', function(){

            beforeEach(function(){

                successSpies();
                initController();
            });

            it('should findData by calling findEmployee',function(){
                $scope.findData();
                // calling $apply to resolve deferred promises we made in the spies
                $scope.$apply();
                expect($scope.loadingEmployee).toEqual(false);
                expect($scope.allEmployees).toEqual(employeesResponse);
            });
        });

        describe('handle controller initialization errors', function(){

            beforeEach(function(){

                rejectedSpies();
                initController();
            });

            it('should handle error when calling findEmployee', function(){
                $scope.findData();
                $scope.$apply();
                // your error expectations
            });
        });
    });
}());