【问题标题】:How to test response from service within controller如何测试控制器内服务的响应
【发布时间】:2016-02-26 09:55:11
【问题描述】:

我正在尝试测试来自控制器内执行的服务 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 请求是否已执行并返回绑定到$scope.result 和vm.banner greeting 的serviceResponse


我试过了:

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.

【问题讨论】:

  • 我认为您应该使用whenGET 而不是expectGET 来模拟响应。此外,您还错过了对 $httpBackend.flush(); 的调用来满足异步请求。但在我看来,您应该在两个单独的测试中进行测试,而不是一个。
  • 你能提供MyService的代码吗?
  • @LouieAlmeda - 向服务提供代码无关紧要,因为我没有测试服务,它只执行获取并返回一些数据,然后我将其绑定到 $scopes。

标签: angularjs unit-testing jasmine karma-jasmine angularjs-controller


【解决方案1】:

我认为问题在于你的服务返回了一个承诺,所以当你这样做时it

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

您的服务在到达expect 之前可能尚未解决,因此您必须先等待您的承诺的then 并在里面执行期望。 您可以做的是将承诺分配给 $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 并期望某些请求并提供模拟响应数据。这是来自角度docs的sn-p

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 调用 /auth.py 时,它都会以模拟数据 {userId: 'userX'}, {'A-Token': 'xxx'} 进行响应

【讨论】:

  • 谢谢,我阅读了该文档。不太适用于我的场景,或者至少它没有通过测试。
【解决方案2】:

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

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

请看下面的代码 cmets 进行解释

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 调用结束时,我们需要等待执行 assert 语句。因此,您可以使用$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);
});

【讨论】:

  • @PankajParker - 谢谢,这是有道理的 - 但我在哪里模拟 serviceResponse.hour?
  • @OamPsy 当前代码看起来不错..如果您使用 $httpBackend.whenGet 而不是 .expectGet 会更好
  • @PankajParker - 我如何模拟它,如何测试虚拟机?
  • @OamPsy 您的做法是正确的。我不明白它有什么问题...?理想情况下,beforeEach 将模拟 $httpBackend 电话..
  • 正如@LouieAlmeda 建议的那样,您应该使用controller.banner.greeting & controller.header,this 控制器上下文将在controller 对象中可用。同时确保您在@ 之前执行$httpBackend.flush() 987654339@(断言声明)
【解决方案3】:

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

控制器测试

在您的控制器中,您不应该关心服务的数据来自哪里,您应该测试返回的结果是否按预期使用。验证您的控制器在初始化时是否调用了所需的方法,以及 $scope.result 和 vm.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 响应的相同资源,为您的服务编写一个单独的测试,调用方法@987654325 @ 执行模拟的 $http 调用并返回所需的结果。

类似:

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

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

【讨论】:

  • 我知道我不应该在控制器中测试服务,这不是我想要做的。我想单独测试我的控制器,但要模拟 http 请求和响应。
  • 与你的期望(vm.banner ...我得到错误:ReferenceError:找不到变量:vm
  • @OamPsy 这正是我认为你不应该做的事! http 请求发生在您的服务中,而不是在您的控制器中。您应该在控制器测试中模拟来自服务的响应,并在服务测试中模拟来自请求的响应。
猜你喜欢
  • 2016-07-11
  • 1970-01-01
  • 2014-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-22
相关资源
最近更新 更多