3

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.
Oam Psy
  • 8,555
  • 32
  • 93
  • 157
  • I think you should be using `whenGET` instead of `expectGET` to simulate the response. Also you're missing a call to `$httpBackend.flush();` to fulfill the async request. BUT in my opinion you should be testing this in two separate tests, not one. – caiocpricci2 Feb 28 '16 at 20:00
  • Can you provide your code for `MyService`? – Louie Almeda Feb 29 '16 at 02:50
  • @LouieAlmeda - provide the code to the service is irrelavant as i am not test the service, it only performs a get and returns some data which i then bind to $scopes. – Oam Psy Feb 29 '16 at 07:52

3 Answers3

3

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

Louie Almeda
  • 5,366
  • 30
  • 38
  • Thanks, ive read that documentation. Not really too applicable to my scenario, or at least it didnt pass a test. – Oam Psy Feb 26 '16 at 10:22
2

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.

caiocpricci2
  • 7,714
  • 10
  • 56
  • 88
  • i know i shouldnt be testing the service in a controller, thats not what i am trying to do. I want to test my controller alone, but mock the http request and response. – Oam Psy Feb 29 '16 at 07:49
  • with your expect(vm.banner... i get the error: ReferenceError: Can't find variable: vm – Oam Psy Feb 29 '16 at 09:05
  • @OamPsy that's precisely what i think you shouldn't do! The http request happens in your service, not in your controller. You should mock the response from the service in the controller test and the response from the request in the service test. – caiocpricci2 Mar 01 '16 at 15:58
2

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);
});
Pankaj Parkar
  • 134,766
  • 23
  • 234
  • 299
  • @PankajParker - thank you, that makes sense - but where do i mock serviceResponse.hour? – Oam Psy Feb 29 '16 at 07:50
  • 1
    @OamPsy current code seems fine..it would be better if you use `$httpBackend.whenGet` instead of `.expectGet` – Pankaj Parkar Feb 29 '16 at 07:54
  • @PankajParker - how do i mock it, and how do i test vm? – Oam Psy Feb 29 '16 at 07:56
  • 1
    @OamPsy the way which you are doing is correct.. I don't understand what is the problem with it...? Ideally `beforeEach` will have mock `$httpBackend` calls.. – Pankaj Parkar Feb 29 '16 at 09:07
  • it is now working, the missing key was Backend.flush - if you can share how to test vm, the 50points are yours. – Oam Psy Feb 29 '16 at 09:18
  • Ive tried expect(controller.vm.banner and expect(vm.header – Oam Psy Feb 29 '16 at 09:21
  • @OamPsy try just using `controller.banner` and `vm.header` – Louie Almeda Feb 29 '16 at 09:39
  • With controller.banner i get Expected undefined to equal 'whatever'... and vm.banner i get ReferenceError: Can't find variable: vm – Oam Psy Feb 29 '16 at 09:47
  • 2
    As @LouieAlmeda suggested, you should use `controller.banner.greeting` & `controller.header`, `this` controller context will be available in `controller` object., Also make sure you are doing `$httpBackend.flush()` before `expect`(Assert statement) – Pankaj Parkar Feb 29 '16 at 09:47
  • Thanks, the problem was 'vm' was not required... contoller.whatever is all that is needed. – Oam Psy Feb 29 '16 at 09:57
  • Correct.. `controller` variable has all information about controller `this` context which you stored in `vm` variable(that was for not loosing `this` to avoid [`this` context related issue](http://stackoverflow.com/a/35647428/2435473)).. I think you are done now. – Pankaj Parkar Feb 29 '16 at 09:59
  • Do you need anything lese from me? – Pankaj Parkar Feb 29 '16 at 18:50