28

After much reading, it seems that the recommended way to call a web service from an AngularJS controller is to use a factory and return a promise from that.

Here I have a simple factory which calls a sample API.

myApp.factory('MyFactory', ['$http',function($http) {
var people = {
        requestPeople: function(x) {
            var url = 'js/test.json';
            return $http.get(url);
        }
    };
return people;
}]);

And this is how I call it in the controller

myApp.controller('MyCtrl1', ['$scope', 'MyFactory', function ($scope, MyFactory) {
        MyFactory.requestPeople(22).then(function(result) {
             $scope.peopleList = result;
        });
}]);

While it works fine, I would like to be able to mock the result that is passed in when then is called. Is this possible?

My attempt so far has produced nothing. This is my attempt:

//Fake service
var mockService = {
    requestPeople: function () {
        return {
            then: function () {
                return {"one":"three"};
            }
        }

    }
};


//Some setup
beforeEach(module('myApp.controllers'));
var ctrl, scope;

beforeEach(inject(function ($rootScope, $controller) {
    scope = $rootScope.$new();

    ctrl = $controller('MyCtrl1', { $scope: scope, MyFactory: mockService });
}));

//Test
it('Event Types Empty should default to false', inject(function () {
    expect(scope.peopleList.one).toBe('three');
}));

The error that I get when running this in karma runner, is

TypeError: 'undefined' is not an object (evaluating 'scope.peopleList.one')

How can I get this test working with my mocked data?

Mendhak
  • 8,194
  • 5
  • 47
  • 64
  • I've read about it, but my controller doesn't take a $http. Can you elaborate on what you mean? – Mendhak Jul 24 '13 at 08:17

1 Answers1

38

I don't think $httpBackend is what you're after here, you want the whole factory to be mocked without it having a dependency on $http?

Take a look at $q, in particular the code sample under the Testing header. Your issue might be resolved with code that looks like this:

'use strict';

describe('mocking the factory response', function () {

    beforeEach(module('myApp.controllers'));

    var scope, fakeFactory, controller, q, deferred;

    //Prepare the fake factory
    beforeEach(function () {
        fakeFactory = {
            requestPeople: function () {
                deferred = q.defer();
                // Place the fake return object here
                deferred.resolve({ "one": "three" });
                return deferred.promise;
            }
        };
        spyOn(fakeFactory, 'requestPeople').andCallThrough();
    });

    //Inject fake factory into controller
    beforeEach(inject(function ($rootScope, $controller, $q) {
        scope = $rootScope.$new();
        q = $q;
        controller = $controller('MyCtrl1', { $scope: scope, MyFactory: fakeFactory });
    }));

    it('The peopleList object is not defined yet', function () {
        // Before $apply is called the promise hasn't resolved
        expect(scope.peopleList).not.toBeDefined();
    });

    it('Applying the scope causes it to be defined', function () {
        // This propagates the changes to the models
        // This happens itself when you're on a web page, but not in a unit test framework
        scope.$apply();
        expect(scope.peopleList).toBeDefined();
    });

    it('Ensure that the method was invoked', function () {
        scope.$apply();
        expect(fakeFactory.requestPeople).toHaveBeenCalled();
    });

    it('Check the value returned', function () {
        scope.$apply();
        expect(scope.peopleList).toBe({ "one": "three" });
    });
});

I've added some tests around what $apply does, I didn't know that until I started playing with this!

Gog

Community
  • 1
  • 1
GogLlundain
  • 552
  • 4
  • 9
  • This worked perfectly. I had come to something similar but what I was missing was the `defer` returning a proper value, and now the `$scope.apply()` makes more sense. – Mendhak Jul 25 '13 at 09:34
  • I've had success basing units tests on this code. However, moving to the "controller as" syntax, it's not clear how to modify the test to pass. The problem appears to be that using `'MyCtrl1 as foo'` is the problem. Even the `not.toBeDefined` tests fails with `TypeError: undefined is not a function`. – jody tate Aug 14 '14 at 18:02
  • 1
    What is the significance of 'spyOn' statement here. I removed it from the test specs and test still passed successfully. – Aakash Aug 19 '14 at 01:37
  • 2
    The problem with this, as far as I can see, is that the promises returned by $http have `success` and `error` helper functions, which a regular `$q` promise lacks. – James Kingsbery Jan 16 '15 at 00:49
  • 1
    @JamesKingsbery `$q` has `.then()` which is probably a better way to handle promises than `.success()`, and it has all the functionality and more. So handle the `$http` result using `.then()` and your handler will also work with `$q`. – steampowered Aug 06 '15 at 22:39