methuselah methuselah - 2 months ago 55
AngularJS Question

Unit test returns TypeError: undefined is not a constructor error message

I am trying to write a simple test in Jasmine to test SearchCtrl. However I am not sure how to structure the unit test because it is my first time using it. This is what I have do so far:

in services.js:

function CategoryService($http) {

this.getCategoryList = function () {

return $http({
method: 'GET',
url: server + '/api/category/',
});

}

}


Sample output:

[
{
"id": "1551742a-5963-4d40-9abb-af7a6fb4ec5d",
"category": "Coach"
}
]


in controller.js:

function SearchCtrl($scope, CategoryService) {

CategoryService.getCategoryList().then(function(dataResponse) {
$scope.categories = dataResponse.data;
});

}


In search.controller.tests.js:

describe('SearchCtrl', function() {

var $controller, scope, categoryServiceMock;

// TODO: Load the app module
beforeEach(module('app.controllers'));
beforeEach(module('app.services'));

// TODO: Instantiate the controller and mocks
beforeEach(inject(function($controller, categoryServiceMock) {
scope = $rootScope.$new();

// mock categoryService
categoryServiceMock = {
login: jasmine.createSpy('category spy')
};

// instantiate SearchCtrl
controller = $controller('SearchCtrl', {
$scope: scope,
'CategoryService': categoryServiceMock
})

}));

describe('#searchList', function() {

// TODO: Call getCategoryList() on the controller
it('should call getCategoryList on categoryService', function() {
expect(categoryServiceMock.getCategoryList).toHaveBeenCalledWith('Coach');
});

describe('once the search is executed', function() {
it('if successful, return a list of categories', function() {

// TODO: Mock response from CategoryService
expect(categoryServiceMock.getCategoryList).toHaveBeenCalledWith('Coach');
});
});
});
});


This currently returns the error:

Error: [$injector:unpr] Unknown provider: categoryServiceMockProvider <- categoryServiceMock


How do I resolve it? Furthermore, have I structured my unit test in the correct way?

UPDATE 25/09

I changed my test to the following:

describe('SearchCtrl', function() {

var scope;
var CategoryServiceMock;
var SearchCtrl;

// Load the app module
beforeEach(module('dingocv.controllers', ['dingocv.services']));

// Define CategoryServiceMock
beforeEach(function() {
CategoryServiceMock = {
getCategoryList: function() {}
};
});

// Inject required services and instantiate the controller
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
SearchCtrl = $controller('SearchCtrl', {
$scope: scope,
CategoryService: CategoryServiceMock
});
}));

// Tests

describe('#searchList', function() {
it('should call registerBook Parse Service method', function () {
spyOn(CategoryServiceMock, 'getCategoryList').andCallThrough();
scope.getCategoryList();
})
});

});


I am now getting this error:

PhantomJS 2.1.1 (Windows 8 0.0.0): Executed 0 of 1 SUCC
PhantomJS 2.1.1 (Windows 8 0.0.0) SearchCtrl #searchList should call registerBook Parse Service method FAILED
forEach@D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:13691:24
loadModules@D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:17878:12
createInjector@D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:17800:30
workFn@D:/myapp-mobile/www/lib/angular-mocks/angular-mocks.js:3074:60
loaded@http://localhost:9876/context.js:151:17
D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:17918:53
TypeError: undefined is not a constructor (evaluating 'spyOn(CategoryServiceMock, 'getCategoryList').andCallThrough()') in unit-tests/search.controller.tests.js (line 30)
unit-tests/search.controller.tests.js:30:67
loaded@http://localhost:9876/context.js:151:17
PhantomJS 2.1.1 (Windows 8 0.0.0): Executed 1 of 1 (1 FPhantomJS 2.1.1 (Windows 8 0.0.0): Executed 1 of 1 (1 FAILED) ERROR (0.04 secs / 0.021 secs)
25 09 2016 09:48:38.940:INFO [watcher]: Changed file "D:/myapp-mobile/www/tests/unit-tests/search.controller.tests.js".
PhantomJS 2.1.1 (Windows 8 0.0.0): Executed 0 of 1 SUCCPhantomJS 2.1.1 (Windows 8 0.0.0) SearchCtrl #searchList should call registerBook Parse Service method FAILED
forEach@D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:13691:24
loadModules@D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:17878:12
createInjector@D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:17800:30
workFn@D:/myapp-mobile/www/lib/angular-mocks/angular-mocks.js:3074:60
loaded@http://localhost:9876/context.js:151:17
D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:17918:53
TypeError: undefined is not a constructor (evaluating 'spyOn(CategoryServiceMock, 'getCategoryList').andCallThrough()') in unit-tests/search.controller.tests.js (line 30)
unit-tests/search.controller.tests.js:30:67
loaded@http://localhost:9876/context.js:151:17
PhantomJS 2.1.1 (Windows 8 0.0.0): Executed 1 of 1 (1 FPhantomJS 2.1.1 (Windows 8 0.0.0): Executed 1 of 1 (1 FAILED) ERROR (0.035 secs / 0.007 secs)
25 09 2016 09:48:42.307:INFO [watcher]: Changed file "D:/myapp-mobile/www/tests/unit-tests/search.controller.tests.js".
PhantomJS 2.1.1 (Windows 8 0.0.0): Executed 0 of 1 SUCCPhantomJS 2.1.1 (Windows 8 0.0.0) SearchCtrl #searchList should call registerBook Parse Service method FAILED
forEach@D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:13691:24
loadModules@D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:17878:12
createInjector@D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:17800:30
workFn@D:/myapp-mobile/www/lib/angular-mocks/angular-mocks.js:3074:60
loaded@http://localhost:9876/context.js:151:17
D:/myapp-mobile/www/lib/ionic/js/ionic.bundle.js:17918:53
TypeError: undefined is not a constructor (evaluating 'spyOn(CategoryServiceMock, 'getCategoryList').andCallThrough()') in unit-tests/search.controller.tests.js (line 30)
unit-tests/search.controller.tests.js:30:67
loaded@http://localhost:9876/context.js:151:17
PhantomJS 2.1.1 (Windows 8 0.0.0): Executed 1 of 1 (1 FPhantomJS 2.1.1 (Windows 8 0.0.0): Executed 1 of 1 (1 FAILED) ERROR (0.069 secs / 0.008 secs)

Answer

You're simply making your tests complex with all those injections. Here's a simple working example for your case:

Service Module

angular.module('dingocv.services', [])
.service('CategoryService', ['$http', function CategoryService($http) {
    this.getCategoryList = function () {
        //Wasn't sure what server was here. So created a variable here. Feel free to change this.
        var server = 'https://www.example.com';
        return $http({
            method: 'GET',
            url: server + '/api/category/',
        });
    }
}]);

Controller Module

angular.module('dingocv.controllers', ['dingocv.services'])
.controller('SearchCtrl', ['$scope', 'CategoryService', function SearchCtrl($scope, CategoryService) {
    CategoryService.getCategoryList().then(function(dataResponse) {
        $scope.categories = dataResponse.data;
    });
}]);

Tests

describe('SearchCtrl', function() {
    var response;
    response = {
        status: 200,
        data: [{
            "id": "1551742a-5963-4d40-9abb-af7a6fb4ec5d",
            "category": "Coach"
        }]
    };

    //Injecting the modules that would then be used in the tests
    beforeEach(module('dingocv.services'));
    beforeEach(module('dingocv.controllers'));

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

        //Creating a spy for this method so that it doesn't call the original Service method.
        spyOn(CategoryService, 'getCategoryList').and.callFake(function(){
            return{
                then: function(successCallback){
                    successCallback(response);
                }
            }
        });

        SearchCtrl = $controller('SearchCtrl', {
            $scope: $scope
        });
    }));

    describe('Initialization', function() {
        it('should initialize the controller\'s scope with categories', function(){
            expect(CategoryService.getCategoryList).toHaveBeenCalled();
            expect($scope.categories).toBeDefined();
            expect($scope.categories.length).toEqual(1);
            expect($scope.categories[0].id).toEqual(response.data[0].id);
            expect($scope.categories[0].category).toEqual(response.data[0].category);
        });
    });
});

Hope this helps.