I am using latest Karma Angular.js unit testing.
I have an error: 'Argument 'appCtrl' is not a function, got undefined'.
appCtrl.js:
var app = angular.module('app', []);
app.controller('appCtrl', function ($scope) {
$scope.count = 5;
$scope.incrementCount = function() {
$scope.count = $scope.count + 1;
};
});
appCtrl.spec.js:
describe('Controller test', function(){
var appCtrl, $scope;
beforeEach(module('app'));
beforeEach(inject(function($controller, $rootScope) {
$scope = $rootScope.$new();
appCtrl = $controller('appCtrl', {
$scope: $scope
});
}));
it('should have appCtrl controller toBeDefined', function() {
expect(appCtrl).toBeDefined();
});
iit('should init counter value', function() {
expect($scope.count).toBeDefined();
expect($scope.count).toBe(5);
});
it('should change counter value', function() {
$scope.incrementCount();
expect($scope.count).toBe(6);
});
});
What I am doing wrong?
Make sure you have the angular-mocks dependency, and also you have a typo 'itt' should be 'it' in the should init counter value.
Hope this helps.
Related
I am trying to test something pretty simple: a controller that calls a service that performs a http request.
Controller:
define(['module'], function (module) {
'use strict';
var MyController = function ($scope, MyService) {
$scope.testScope = 'karma is working!';
MyService.getData().then(function (data) {
$scope.result = data.hour
});
};
module.exports = ['$scope', 'MyService', MyController ];
});
Test:
define(['require', 'angular-mocks'], function (require) {
'use strict';
var angular = require('angular');
describe("<- MyController Spec ->", function () {
var controller, scope, myService, serviceResponse;
serviceResponse= {
id: 12345,
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!');
});
});
});
With the above, i get the error:
TypeError: 'undefined' is not an object (evaluating 'spyOn(myService, 'getData').and.returnValue')
Solved the question - was using Jasmine 1x.. Upgraded to 2.0 and works as expected
Controller:
angular.module('ngApp', [
'templates-app',
'templates-common',
'ngApp.home',
'ui.router'
])
.config(function myAppConfig($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/home');
})
.run(function run() {})
.controller('AppCtrl', function AppCtrl($scope) {
$scope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
if (angular.isDefined(toState.data.pageTitle)) {
$scope.pageTitle = toState.data.pageTitle + ' | App';
}
});
});
Test:
describe('AppCtrl', function() {
beforeEach(module('ngApp'));
var $controller;
beforeEach(inject(function(_$controller_) {
$controller = _$controller_;
}));
describe('$scope.pageTitle', function() {
it('should set pageTitle', inject(function() {
var $scope = {};
var AppCtrl = $controller('AppCtrl', { $scope: $scope });
expect(true).toBe(true);
}));
});
});
I am not testing anything right now, just loading the controller. However this fails
Chrome 41.0.2272 (Mac OS X 10.10.2) AppCtrl $scope.pageTitle should set pageTitle FAILED
TypeError: undefined is not a function
at new AppCtrl (/src/app/app.js:15:9)
at invoke (/vendor/angular/angular.js:4203:17)
at Object.instantiate (/vendor/angular/angular.js:4211:27)
at /vendor/angular/angular.js:8501:28
at /vendor/angular-mocks/angular-mocks.js:1878:12
at Object.<anonymous> (/src/app/app.spec.js:14:19)
at Object.invoke (/vendor/angular/angular.js:4203:17)
at Object.workFn (/vendor/angular-mocks/angular-mocks.js:2436:20)
Error: Declaration Location
at window.inject.angular.mock.inject (/vendor/angular-mocks/angular-mocks.js:2407:25)
at Suite.<anonymous> (/src/app/app.spec.js:12:30)
at Suite.<anonymous> (/src/app/app.spec.js:11:2)
at /src/app/app.spec.js:1:1
Any ideas whats wrong with my test setup?
Actual test:
describe('$scope.pageTitle', function() {
it('should be defiend after stateChange', inject(function() {
var $scope = {};
var AppCtrl = createController('AppCtrl');
scope.$broadcast('$stateChangeSuccess', {
data: {
pageTitle: 'bar'
}
});
expect(scope.pageTitle).toBeDefined();
}));
});
You need to create the scope from the $rootScope and inject it:
beforeEach(inject(function (_$controller_, $rootScope) {
$controller = _$controller_;
scope = $rootScope.$new();
createController = function (ctrlName) {
return $controller(ctrlName, {
'$scope': scope
});
};
}));
In this way you can use the createController function for the purpose and using:
var AppCtrl = createController('AppCtrl');
For having the controller.
Here is a working jsfiddle:
http://jsfiddle.net/Lbs6sk0k/2/
EDIT: where is $scope empty?
if I put a console.log here:
.controller('AppCtrl', function AppCtrl($scope) {
console.log($scope);
I can see this:
EDIT 2:
Set the the scope as a global variable:
describe('AppCtrl', function () {
beforeEach(module('ngApp'));
var $controller;
var scope;
Then, in you test case, you can use it:
it('should set pageTitle', inject(function () {
var $scope = scope;
Updated jsfiddle with your new test case: http://jsfiddle.net/Lbs6sk0k/3/
I think the problem is with the scope you're injecting into the controller and the $scope.$on... . Try this:
describe('AppCtrl', function() {
beforeEach(module('ngApp'));
var $controller, $scope;
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_.$new();
}));
describe('$scope.pageTitle', function() {
it('should set pageTitle', inject(function() {
var AppCtrl = $controller('AppCtrl', { $scope: $scope });
expect(true).toBe(true);
}));
});
});
I'm trying to test a controller than requires me to mock a service that I'm using to get data. Currently I'm getting an error saying the function is undefined on this line:
dataServiceMock = jasmine.createSpyObj('dataService', ['getFunctionStuff']);
According to other examples and tutorials this should be working fine.
Here's my code including the test file, the service and the controller.
Controller:
var app = angular.module('myApp', []);
app.controller('MainCtrl', function($scope, dataService) {
dataService.getFunctionStuff($scope.foo)
.then(function(data) {
$scope.test = data;
});
});
Service:
app.factory('dataService', function ($timeout, $q){
function getFunctionStuff(formData) {
return $http.post('../randomAPICall', formData).then(function(data) {
return data;
});
};
});
Tests:
describe('Testing a controller', function() {
var $scope, ctrl, $timeout;
var dataServiceMock;
beforeEach(function (){
dataServiceMock = jasmine.createSpyObj('dataService', ['getFunctionStuff']);
module('myApp');
inject(function($rootScope, $controller, $q, _$timeout_) {
$scope = $rootScope.$new();
dataServiceMock.getFunctionStuff.and.ReturnValue($q.when('test'));
$timeout = _$timeout_;
ctrl = $controller('MainCtrl', {
$scope: $scope,
dataService: dataServiceMock
});
});
});
it('should update test', function (){
expect($scope.test).toEqual('test');
});
});
Here's a plunker of it: http://plnkr.co/edit/tBSl88RRhj56h3Oiny6S?p=preview
As you are using jasmine 2.1, the API is .and.returnValue. And in your test spec, do $scope.$apply() before then
describe('Testing a controller', function () {
var $scope, ctrl, $timeout;
var dataServiceMock;
beforeEach(function () {
dataServiceMock = jasmine.createSpyObj('dataService', ['getFunctionStuff']);
module('myApp');
inject(function ($rootScope, $controller, $q, _$timeout_) {
$scope = $rootScope.$new();
dataServiceMock.getFunctionStuff.and.returnValue($q.when('test'));
$timeout = _$timeout_;
ctrl = $controller('MainCtrl', {
$scope: $scope,
dataService: dataServiceMock
});
});
});
it('should update test', function () {
$scope.$apply();
expect($scope.test).toEqual('test');
});
});
Here is another common way to test $http by $httpBackend:
app.js
var app = angular.module('myApp', []);
app.controller('MainCtrl', function($scope, dataService) {
dataService.getFunctionStuff($scope.foo)
.then(function(data) {
$scope.test = data.data;
});
});
dataService.js
app.factory('dataService', function($http) {
function getFunctionStuff(formData) {
return $http.post('../randomAPICall', formData).then(function(data) {
return data;
});
}
return {
getFunctionStuff: getFunctionStuff
};
});
specs.js
describe('Testing a controller', function() {
var $scope, ctrl, $controller, $httpBackend;
beforeEach(function (){
module('myApp');
inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$scope = $injector.get('$rootScope').$new();
$controller = $injector.get('$controller');
$scope.foo = 'foo';
$httpBackend.expectPOST('../randomAPICall', 'foo').respond(201, 'test');
ctrl = $controller('MainCtrl', {$scope: $scope});
});
});
it('should update test', function (){
expect($scope.test).not.toBeDefined();
$httpBackend.flush();
expect($scope.test).toEqual('test');
});
});
I'm trying to test the behavior of a 'watch' on my scope. But the problem is it's called only once. Check the fiddle below. And strangely, i noticed that if you put the variables in the controller's $scope rather than on the controller itself (aka $scope.name vs vm.name) it actually works.
Using vm (not working): http://jsfiddle.net/2Ny8x/60/
//--- CODE --------------------------
(function (angular) {
// Create module
var myApp = angular.module('myApp', []);
// Controller which counts changes to its "name" member
myApp.controller('MyCtrl', ['$scope', function ($scope) {
var vm = this;
vm.name = 'Superhero';
vm.counter = 0;
$scope.$watch('vm.name', function (newValue, oldValue) {
vm.counter = vm.counter + 1;
});
}]);
})(angular);
// ---SPECS-------------------------
describe('myApp', function () {
var scope,
controller;
beforeEach(function () {
module('myApp');
});
describe('MyCtrl', function () {
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller('MyCtrl', {
'$scope': scope
});
}));
it('sets the name', function () {
expect(controller.name).toBe('Superhero');
});
it('watches the name and updates the counter', function () {
expect(controller.counter).toBe(0);
controller.name = 'Batman';
scope.$digest();
expect(controller.counter).toBe(1);
controller.name = 'Superman';
scope.$digest();
expect(controller.counter).toBe(2);
});
});
});
Using $scope (works): http://jsfiddle.net/2Ny8x/61/
//--- CODE --------------------------
(function (angular) {
// Create module
var myApp = angular.module('myApp', []);
// Controller which counts changes to its "name" member
myApp.controller('MyCtrl', ['$scope', function ($scope) {
$scope.name = 'Superhero';
$scope.counter = 0;
$scope.$watch('name', function (newValue, oldValue) {
$scope.counter = $scope.counter + 1;
});
}]);
})(angular);
// ---SPECS-------------------------
describe('myApp', function () {
var scope,
controller;
beforeEach(function () {
module('myApp');
});
describe('MyCtrl', function () {
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller('MyCtrl', {
'$scope': scope
});
}));
it('sets the name', function () {
expect(scope.name).toBe('Superhero');
});
it('watches the name and updates the counter', function () {
expect(scope.counter).toBe(0);
scope.name = 'Batman';
scope.$digest();
expect(scope.counter).toBe(1);
scope.name = 'Superman';
scope.$digest();
expect(scope.counter).toBe(2);
});
});
});
You are watching for a property on the scope, which is not really the case, since you are modifying property in your controller. So change your watch to:-
$scope.$watch(function(){
return vm.name;
}, function (newValue, oldValue) {
vm.counter = vm.counter + 1;
});
Plnkr
Another way to make it work is to use controller as vm in your test while instantiating the controller, so that controller instance would be attached the scope as a property with name vm.
controller = $controller('MyCtrl as vm', {
'$scope': scope
});
Plnkr2
I am unit testing a controller and I want to test an event handler. Say my controller looks like:
myModule.controller('MasterController', ['$scope', function($scope){
$scope.$on('$locationChangeSuccess', function() {
$scope.success = true;
});
}]);
Would I broadcast that in my Jasmine test? Would I emit it? Is there an accepted standard?
The solution I came up with is as follows:
describe('MasterController', function() {
var $scope, $rootScope, controller, CreateTarget;
beforeEach(function() {
inject(function($injector) {
$rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
var $controller = $injector.get('$controller');
CreateTarget = function() {
$controller('MasterController', {$scope: $scope});
}
});
});
describe('$locationChangeSuccess', function() {
it('should set $scope.success to true', function() {
controller = CreateTarget();
$rootScope.$broadcast('$locationChangeSuccess');
expect($scope.success).toBe(true);
});
});
});
I don't think there is "an accepted standard" but according to $location source code the event is broadcasted, so I would mock this behavior and test it this way:
'use strict';
describe('MasterController', function() {
var MasterController,
$rootScope,
$scope;
beforeEach(module('myModule'));
beforeEach(inject(function($rootScope, $injector, $controller) {
$rootScope = $rootScope;
$scope = $rootScope.$new();
MasterController = $controller('MasterController', {
'$scope': $scope
});
$scope.$digest();
}));
describe('$locationChangeSuccess event listener', function() {
it('should set $scope.success to true', function() {
var newUrl = 'http://foourl.com';
var oldUrl = 'http://barurl.com'
$scope.$apply(function() {
$rootScope.$broadcast('$locationChangeSuccess', newUrl, oldUrl);
});
expect($scope.success).toBe(true);
});
});
});