Oam Psy
Oam Psy

Reputation: 8663

How to test response from service within controller

I am trying to test the response from a service http call that is performed within a controller:

Controller:

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

Unit test:

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

How can i test the http request is performed and returns serviceResponse which binds to $scope.result and vm.banner greeting


Ive tried:

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

With the error:

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

Upvotes: 3

Views: 3356

Answers (3)

Louie Almeda
Louie Almeda

Reputation: 5632

I think the problem is that your service returns a promise, so when you do this it

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

your service may not have been resolved yet before it reach the expect, so you'll have to wait for the then of your promise first and do the expect inside. what you can do is assign to promise to $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
        }
    });
};    

then in your test, you can do something like

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

});

You will need to mock $httpBackend and expect for certain request and provide mock response data. here's a snippet from 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'});

   }));

now, whenever $http will call a get to /auth.py it will respond with the mocked data {userId: 'userX'}, {'A-Token': 'xxx'}

Upvotes: 3

Pankaj Parkar
Pankaj Parkar

Reputation: 136174

What I understand is while expecting for result, you have made an Ajax & that sets data value when promise get resolved.

Basically when you call myService.getData() method on controller instantiate, it do an $http, which will indirectly do $httpBackend ajax and will return mockData from it. But as soon as you make a call you are not waiting for call that call to get completed & then you are calling your assert statement's which fails your statte.

Look at below code comments for explanation

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

So now you understand somehow you need to tell our test code that, we need to wait to execute assert statement when ajax call gets over. So for that you could use $httpBackend.flush() method what that will help you in that case. It will clear out all $httpBackend calls queue clear, before passing control to next line.

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

Upvotes: 2

caiocpricci2
caiocpricci2

Reputation: 7798

You shouldn't be testing the behavior of your service in your controller. Test the controller and the service in separate tests.

Controller test

In your controller you shouldn't care where the data from the service came from, you should test that the returned result is being used as expected. Verify that your controller calls the desired method when it's inited and that the values of $scope.result and vm.bannerare what you expect.

Something like:

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

Service test

Your service test in the other hand should be aware of the $http call and should be validating the response, so using the same resources from @Luie Almeda response, write a separate test for your service, calling the method getData() that performs the mocked $http call and returns the desired result.

Something like:

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

You need to replace GET_DATA_URL with the correct url being called by MyService.getData(). Always try to keep your tests in the same modules as your code. Test your controller code in your controller tests and your service code in your service tests.

Upvotes: 2

Related Questions