如何测试控制器内服务的响应

How to test response from service within controller

我正在尝试测试在控制器中执行的服务 http 调用的响应:

控制器:

define(['module'], function (module) {
    'use strict';

    var MyController = function ($scope, MyService) {

        var vm = this;

        $scope.testScope = 'karma is working!';

        MyService.getData().then(function (data) {
            $scope.result = data.hour
            vm.banner = {
                'greeting': data.greeting
            }
        });
    };    

    module.exports = ['$scope', 'MyService', MyController ];
});

单元测试:

define(['require', 'angular-mocks'], function (require) {
'use strict';

var angular = require('angular');

describe("<- MyController Spec ->", function () {    

    var controller, scope, myService, serviceResponse;

    serviceResponse= {
        greeting: 'hello',
        hour: '12'
    };

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

    beforeEach(inject(function (_$controller_, _$rootScope_, _MyService_, $q) {
        scope = _$rootScope_.$new();
        var deferred = $q.defer();
        deferred.resolve(serviceResponse);

        myService = _MyService_;
        spyOn(myService, 'getData').and.returnValue(deferred.promise);

        controller = _$controller_('MyController', {$scope: scope});  
        scope.$apply();
    }));

    it('should verify that the controller exists ', function() {
        expect(controller).toBeDefined();
    });    

    it('should have testScope scope equaling *karma is working*', function() {
        expect(scope.testScope ).toEqual('karma is working!');
    });
});
});

我如何测试 http 请求是否执行以及 returns serviceResponse 绑定到 $scope.resultvm.banner greeting


我试过:

define(['require', 'angular-mocks'], function (require) {
'use strict';

var angular = require('angular');

describe("<- MyController Spec ->", function () {    

    var controller, scope, myService, serviceResponse, $httpBackend;

    serviceResponse= {
        greeting: 'hello',
        hour: '12'
    };

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

    beforeEach(inject(function (_$controller_, _$rootScope_, _MyService_, _$httpBackend_) {
        scope = _$rootScope_.$new();
        $httpBackend = _$httpBackend_

        $httpBackend.expectGET("/my/endpoint/here").respond(serviceResponse);

        myService = _MyService_;
        spyOn(myService, 'getData').and.callThrough();

        controller = _$controller_('MyController', {$scope: scope});  
        scope.$apply();
    }));

    it('should call my service and populate scope.result ', function() {
        myService.getData();
        expect(scope.result ).toEqual(serviceResponse.hour);
    });

    it('should verify that the controller exists ', function() {
        expect(controller).toBeDefined();
    });    

    it('should have testScope scope equaling *karma is working*', function() {
        expect(scope.testScope ).toEqual('karma is working!');
    });
});
});

出现错误:

[should call my service and populate scope.result -----     Expected undefined to be defined.

我认为问题是你的服务 returns 一个承诺,所以当你这样做时 it

it('should call my service and populate scope.result ', function() {
    myService.getData();
    expect(scope.result ).toEqual(serviceResponse.hour);
});

您的服务在到达 expect 之前可能尚未解决,因此您必须先等待您的 promise then 并在其中执行 expect。 您可以做的是将承诺分配给 $scope.result

var MyController = function ($scope, MyService) {

    var vm = this;

    $scope.testScope = 'karma is working!';

    $scope.result = MyService.getData().then(function (data) {
        $scope.result = data.hour
        vm.banner = {
            'greeting': data.greeting
        }
    });
};    

然后在你的测试中,你可以做类似

的事情
it('should call my service and populate scope.result ', function() {
    //myService.getData(); <- you don't have to call this
    scope.result.then(function(){
       expect(scope.result).toEqual(serviceResponse.hour);
    });

});

您将需要模拟 $httpBackend 并期望某些请求并提供模拟响应数据。这是来自 angular docs

的片段
beforeEach(inject(function($injector) {
     // Set up the mock http service responses
     $httpBackend = $injector.get('$httpBackend');

     // backend definition common for all tests    
     $httpBackend.when('GET', '/auth.py')
                            .respond({userId: 'userX'}, {'A-Token': 'xxx'});

   }));

现在,只要 $http 调用 get 到 /auth.py,它就会用模拟数据进行响应 {userId: 'userX'}, {'A-Token': 'xxx'}

您不应该在控制器中测试服务的行为。在单独的测试中测试控制器和服务。

控制器测试

在您的控制器中,您不应该关心来自服务的数据来自何处,您应该测试返回的结果是否按预期使用。验证您的控制器在初始化时是否调用了所需的方法,并且 $scope.resultvm.banner 的值是否符合您的预期。

类似于:

it('should have called my service and populated scope.result ', function() {
    expect(myService.getData).toHaveBeenCalled();
    expect(scope.result).toEqual(serviceResponse.hour);
    expect(vm.banner).toEqual({greeting:serviceResponse.greeting});
});

服务测试

另一方面,您的服务测试应该知道 $http 调用并且应该验证响应,因此使用来自@Luie Almeda 响应的相同资源,为您的服务编写一个单独的测试,调用执行模拟 $http 调用的方法 getData() 和 returns 所需的结果。

类似于:

it('Should return serviceResponse', function () {
    var  serviceResponse= {
      greeting: 'hello',
      hour: '12'
    };
    var myData,

    $httpBackend.whenGET('GET_DATA_URL').respond(200, userviceResponse);

    MyService.getData().then(function (response){
      myData = response
    })

    $httpBackend.flush();

    expect(myData).toBe(serviceResponse);
});

您需要用 MyService.getData() 调用的正确 url 替换 GET_DATA_URL。始终尝试将测试保持在与代码相同的模块中。在控制器测试中测试控制器代码,在服务测试中测试服务代码。

我的理解是,在期待结果的同时,您已经创建了一个 Ajax & 在 promise 得到解决时设置数据值。

基本上,当您在控制器实例化上调用 myService.getData() 方法时,它会执行 $http,这将间接执行 $httpBackend ajax 并将 return mockData 从中。但是,一旦您拨打电话,您就不会等待呼叫完成,然后您正在呼叫您的 assert 声明,这使您的状态失败。

看下面代码注释解释

it('should call my service and populate scope.result ', function() {
   //on controller instantiation only we make $http call
   //which will make your current execution state of your code in async state
   //and javascript transfer its execution control over next line which are assert statement
   //then obiviously you will have undefined value.
   //you don't need to call below line here, as below method already called when controller instantiated
   //myService.getData();
   //below line gets called without waiting for dependent ajax to complete.
   expect(scope.result ).toEqual(serviceResponse.hour);
});

所以现在你明白了你需要告诉我们的测试代码,我们需要等待 ajax 调用结束时执行断言语句。因此,您可以使用 $httpBackend.flush() 方法在这种情况下对您有帮助。在将控制传递给下一行之前,它将清除所有 $httpBackend 调用队列清除。

it('should call my service and populate scope.result ', function() {
   $httpBackend.flush();// will make sure above ajax code has received response
   expect(scope.result ).toEqual(serviceResponse.hour);
});