I followed this post (http://gonehybrid.com/how-to-write-automated-tests-for-your-ionic-app-part-2/) to create a simple unit test using Karma & Jasmine for a Ionic controller, but i keep getting undefined errors while the stated objects have been defined. I'm i missing something obvious? By the way, i'm able to run referenced tests from the blog above successfully which makes me think i'm missing something in mine.
Errora are as follows:
TypeError: undefined is not an object (evaluating 'authMock.login') in /Users/projects/app/tests/unit-tests/login.controller.tests.js (line 65)
TypeError: undefined is not an object (evaluating 'deferredLogin.resolve') in /Users/projects/app/tests/unit-tests/login.controller.tests.js (line 71)
TypeError: undefined is not an object (evaluating 'deferredLogin.reject') in /Users/projects/app/tests/unit-tests/login.controller.tests.js (line 79)
Here's the controller:
angular.module('app').controller('LoginCtrl', function($scope, $state, $ionicPopup, $auth) {
$scope.loginData = {};
$scope.user = {
email: '',
password: ''
};
$scope.doLogin = function(data) {
$auth.login(data).then(function(authenticated) {
$state.go('app.tabs.customer', {}, {reload: true});
}, function(err) {
var alertPopup = $ionicPopup.alert({
title: 'Login failed!',
template: 'Please check your credentials!'
});
});
};
});
Here's the test:
describe('LoginCtrl', function() {
var controller,
deferredLogin,
$scope,
authMock,
stateMock,
ionicPopupMock;
// load the module for our app
beforeEach(angular.mock.module('app'));
// disable template caching
beforeEach(angular.mock.module(function($provide, $urlRouterProvider) {
$provide.value('$ionicTemplateCache', function(){} );
$urlRouterProvider.deferIntercept();
}));
// instantiate the controller and mocks for every test
beforeEach(angular.mock.inject(function($controller, $q, $rootScope) {
deferredLogin = $q.defer();
$scope = $rootScope.$new();
// mock dinnerService
authMock = {
login: jasmine.createSpy('login spy')
.and.returnValue(deferredLogin.promise)
};
// mock $state
stateMock = jasmine.createSpyObj('$state spy', ['go']);
// mock $ionicPopup
ionicPopupMock = jasmine.createSpyObj('$ionicPopup spy', ['alert']);
// instantiate LoginController
controller = $controller('LoginCtrl', {
'$scope': $scope,
'$state': stateMock,
'$ionicPopup': ionicPopupMock,
'$auth': authMock
});
}));
describe('#doLogin', function() {
// call doLogin on the controller for every test
beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
var user = {
email: 'test#yahoo.com',
password: 'test'
};
$scope.doLogin(user);
}));
it('should call login on $auth Service', function() {
expect(authMock.login).toHaveBeenCalledWith(user);
});
describe('when the login is executed,', function() {
it('if successful, should change state to app.tabs.customer', function() {
deferredLogin.resolve();
$rootScope.$digest();
expect(stateMock.go).toHaveBeenCalledWith('app.tabs.customer');
});
it('if unsuccessful, should show a popup', function() {
deferredLogin.reject();
$rootScope.$digest();
expect(ionicPopupMock.alert).toHaveBeenCalled();
});
});
})
});
Here's my Karma config:
files: [
'../www/lib/ionic/js/ionic.bundle.js',
'../www/lib/angular-mocks/angular-mocks.js',
'../www/js/*.js',
'../www/js/**/*.js',
'unit-tests/**/*.js'
],
I think that your controller for tests is undefined. Try to replace first it function with this and check if is it defined.
it('controller to be defained', function() {
expect($controller).toBeDefined();
});
If it isn't, try to call controller with:
$controller = _$controller_;
Related
I am trying to do unit test of my angular app with karma. I am getting some error. Am i missing something? A
This my controller
(function () {
"use strict"
angular
.module("myApp")
.controller("userCtrl",['$scope', '$state', 'userService', 'appSettings','md5','currentUser','$rootScope',
function ($scope, $state, userService, appSettings,md5,currentUser, $rootScope) {
$scope.login = function() {
$scope.loading = true;
if($scope.password != null){
var user ={
username:$scope.username,
password:md5.createHash($scope.password)
}
var getData = userService.login(user);
getData.then(function (response) {
console.log(response);
$scope.loading = false;
currentUser.setProfile(user.username, response.data.sessionId);
$state.go('videos');
}, function (response) {
console.log(response.data);
});
}else{
$scope.msg = "Password field is empty!"
}
}
}])
}());
This is my test codes
'use strict';
describe('userCtrl', function() {
beforeEach(module('myApp'));
var scope, userCtrl, apiService,q, deferred, currentUser;
describe('$scope.login', function(){
beforeEach(function(){
apiService = {
login: function () {
deferred = q.defer();
return deferred.promise;
};
};
});
beforeEach(inject(function($controller, $rootScope, $q, _currentUser_){
var user ={name:'ali',password:'password'};
scope = $rootScope.$new();
q = $q;
// The injector unwraps the underscores (_) from around the parameter names when matching
userCtrl = $controller('userCtrl', {
$scope:scope,
userService:apiService
});
//userService = _userService_;
currentUser = _currentUser_;
}));
it('should call user service login', function() {
spyOn(apiService, 'login').and.callThrough();
scope.login();
deferred.resolve(user);
expect(apiService.login).toHaveBeenCalled();
});
it('checks the password field', function() {
scope.login();
expect(scope.msg).toEqual('Password field is empty!');
});
});
});
And i am getting this error
enter image description here
If you have to test controller then use to spyon for service method and in case of service then use HttpBackend
describe('Testing a Controller that uses a Promise', function() {
var $scope;
var $q;
var deferred;
beforeEach(module('search'));
beforeEach(inject(function($controller, _$rootScope_, _$q_, searchService) {
$q = _$q_;
$scope = _$rootScope_.$new();
// We use the $q service to create a mock instance of defer
deferred = _$q_.defer();
// Use a Jasmine Spy to return the deferred promise
spyOn(searchService, 'search').and.returnValue(deferred.promise);
// Init the controller, passing our spy service instance
$controller('SearchController', {
$scope: $scope,
searchService: searchService
});
}));
it('should resolve promise', function() {
// Setup the data we wish to return for the .then function in the controller
deferred.resolve([{
id: 1
}, {
id: 2
}]);
// We have to call apply for this to work
$scope.$apply();
// Since we called apply, not we can perform our assertions
expect($scope.results).not.toBe(undefined);
expect($scope.error).toBe(undefined);
});
});
This for same using spyon for service method then use $appy method to make it work.
I'm trying to mock the resolve functions inside the $state of my ui-router file, but I can't seem to get it to work. Here's my router code:
$stateProvider
.state('state.of.page', {
url: '/url-to-page',
template: require('./page-template.html'),
controller: 'PageCtrl',
controllerAs: 'page',
resolve: {
/**
* Cloning of page object in case user leaves page
* without saving
*/
pageObjClone: ['pageObj', function (pageObj) {
return angular.copy(pageObj);
}],
pageTemplate: ['template', function (template) {
return template;
}]
}
Here is my Jasmine code. I'm currently getting the error 'fn' is not a function when I run the test.
'use strict';
describe('my jasmine $state test', function() {
var $state;
var $injector;
var stateName = 'state.of.page';
var stateObj;
beforeEach(function() {
angular.mock.module('MyApp');
inject(function(_$rootScope_, _$state_, _$injector_) {
$state = _$state_;
$injector = _$injector_;
stateObj = {
customerObjClone: ['customerObj', function (customerObj) {}],
template: ['template', function (template) {}]
};
});
});
it('should resolve data', function() {
var state = $state.get(stateName);
expect($injector.invoke($state.resolve)).toBe('stateObj');
});
});
Thanks for any help.
...
var pageObj, template;
beforeEach(function() {
angular.mock.module('MyApp');
inject(function(..., _pageObj_, _template_) {
...
pageObj = _pageObj_;
template = _template_;
});
});
it('should resolve data', function() {
var state = $state.get(stateName);
expect($injector.invoke(state.resolve.pageObjClone)).toEqual(pageObj);
expect($injector.invoke(state.resolve.pageTemplate)).toBe(template);
});
Depending on how complex resolvers and their dependencies are, the dependencies may be mocked and injected into resolver, e.g.
expect($injector.invoke(state.resolve.pageTemplate, null, { template: mockedTemplate }))
.toBe(mockedTemplate);
I just switched my application from using the Jade template engine to use client side HTML in order to improve performance and decrease server requests. Everything is working fine in the application however I'm having an issue updating my unit tests.
I have the following test:
describe('Registration Controller Tests', function() {
var $controller, $scope, defer, registerSpy, doesUserExistSpy, auth, RegistrationCtrl;
beforeEach(module('enigmaApp'));
beforeEach(inject(function (_$controller_, _$rootScope_, $q) {
$controller = _$controller_;
$scope = _$rootScope_;
defer = $q.defer();
// Create spies
registerSpy = jasmine.createSpy('register').and.returnValue(defer.promise);
doesUserExistSpy = jasmine.createSpy('doesUserExist').and.returnValue(defer.promise);
auth = {
register: registerSpy,
doesUserExist: doesUserExistSpy
};
// Init register controller with mocked services and scope
RegistrationCtrl = $controller('RegistrationCtrl', {
$scope: $scope,
auth: auth
});
// digest to update controller with services and scope
$scope.$digest();
}));
describe('RegistrationCtrl.register()', function () {
beforeEach(function () {
$scope.user = {
email: 'bwayne#wayneenterprise.com',
first_name: 'Bruce',
last_name: 'Wyane',
password: 'password123'
}
});
it('should call auth.register() with $scope.user', function () {
$scope.register();
expect(auth.register).toHaveBeenCalledWith($scope.user);
});
});
Which results in the following error:
Error: Unexpected request: GET modules/home/home.html
No more requests expected
Any ideas what I need to do in order to mock the routes? I've tried a few things but nothings working so far.
Additional code:
RegistrationCtrl
.controller('RegistrationCtrl', function($scope, $state, auth) {
$scope.user = {};
$scope.userExists = false;
$scope.error = '';
$scope.register = function() {
auth.register($scope.user)
.then(function(response){
$state.go('secure.user');
})
.catch(function(err){
$scope.error = err;
});
};
});
assuming your static files are all in /modules:
$httpBackend.whenGET(/modules\/[\w\W]*/).passThrough();
I've been slowly updating an angular app ready for a 2.0 migration when it's released and I'm running into issues updating my specs. The main problem is I've started to use the controller as syntax and remove scope from my controllers completely but now I'm having problems spying on services that are called in the context of 'this' inside the controller.
I'm copying below the controller code and the spec. I've dummed it down so we can try and solve this problem easily.
Controller:
var LoginCtrl;
LoginCtrl = (function() {
function LoginCtrl(Auth, $state, Session) {
this.Auth = Auth;
this.$state = $state;
this.Session = Session;
this.credentials = {
username: undefined,
password: undefined,
email: undefined
};
}
LoginCtrl.prototype.login = function () {
var _self = this;
this.Auth.login(this.credentials).then(function(response) {
console.log(response)
}, function(error) {
console.log(error)
});
};
return LoginCtrl;
})();
angular.module('projectx').controller('LoginCtrl', ['Auth', '$state', 'Session', LoginCtrl]);
Spec:
describe('Controller: LoginCtrl', function () {
var $controller, Auth;
beforeEach(module('projectx'));
beforeEach(inject(function(_$controller_, _Auth_) {
$controller = _$controller_;
Auth = _Auth_;
}));
it('should pass user credentials to server to log you in', function() {
var scope = {};
var login = $controller('LoginCtrl as login', {$scope: scope});
expect(scope.login).toBe(login);
spyOn(scope.login.Auth, 'login');
scope.login.credentials = {
username: 'test',
password: 'test'
};
scope.login.login();
expect(scope.login.Auth.login).toHaveBeenCalledWith({username: 'test', password: 'test'});
});
});
The error I get is 'undefined' when the controller trys to call this.Auth.login because my spec is spying on (scope.login.Auth, 'login') which is a separate instance I think? Heres the actual error:
PhantomJS 1.9.8 (Mac OS X) Controller: LoginCtrl should pass user credentials to server to log you in FAILED
TypeError: 'undefined' is not an object (evaluating 'this.Auth.login(this.credentials).then')
at /app/controllers/login.js:25
at /spec/controllers/login.js:49
I am using AngularJs controller to send data from form to Google Sheet. Using Jasmine I wrote unit-test, which raises the bellow issue:
Error: Unsatisfied requests: POST http://localhost:5000/google-form
at Function.$httpBackend.verifyNoOutstandingExpectation
(.../angular-mocks/angular-mocks.js:1474:13)
After googling and going through some questions in stackowerflow, I decided to post the question as I didn't find a solution for it.
Here is the code for your references:
Angular Controller
/* global $ */
'use strict';
angular.module('myApp')
.controller('QuickMessageCtrl', ['$scope', function ($scope) {
$scope.quickMessageButtonText = 'Send';
$scope.quickMessage = {
name: '',
email: '',
content: '',
};
function setSubmittingIndicators() {
$scope.quickMessageButtonText = '';
$scope.submitting = true;
}
$scope.postQuickMessageToGoogle = _.throttle(function() {
setSubmittingIndicators();
$.ajax({
url: 'https://docs.google.com/forms/d/MyFormKey/formResponse',
data: {
'entry.3' : $scope.quickMessage.name,
'entry.1' : $scope.quickMessage.email,
'entry.0' : $scope.quickMessage.content
},
type: 'POST',
dataType: 'jsonp',
statusCode: {
200: function (){
//show succes message;
}
}
});
}, 500);
}]);
Unit Test Code
'use strict';
describe('Controller: QuickMessageCtrl', function() {
var $httpBackend, $rootScope, $controller, scope, apiUrl;
beforeEach(module('myApp'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
apiUrl = $injector.get('apiUrl');
$httpBackend.expect(
'POST',
apiUrl + 'google-form',
{'name': 'test', 'email': 'test#test.com', 'content': 'this is content'}
).respond(200);
$rootScope = $injector.get('$rootScope');
scope = $rootScope.$new();
$controller = $injector.get('$controller');
$controller('QuickMessageCtrl', { $scope: scope });
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('Successful form submit', function() {
beforeEach(function() {
scope.quickMessageForm = { $valid: true };
scope.quickMessage.email = 'test#test.com';
scope.quickMessage.name = 'test';
scope.quickMessage.content = 'this is test';
scope.postQuickMessageToGoogle();
});
it('should set submitting indicators on submit', function() {
expect(scope.quickMessageButtonText).toBe('');
});
});
});
Your tests says that the mock http backend should receive a POST at the URL
apiUrl + 'google-form'
which, given the error message, is http://localhost:5000/google-form.
But your controller never sends a POST to that URL. It sends a POST to https://docs.google.com/forms/d/MyFormKey/formResponse. And it doesn't do it using angular's $http service, but does it behind its back, using jQuery.
As #JB Nizet pointed you are using jQuery instead of angular methods. In fact you should refactor your code a bit.
Its a great practice to keep things separate, like Controller from Service. In your case you are using service inside the controller. I would rather suggest you to create a service and then import that service in your controller. So basically here how the code will look like:
Controller
/* global $ */
'use strict';
angular.module('myApp')
.controller('QuickMessageCtrl', ['$scope', 'MyNewService', function ($scope, MyNewService) {
$scope.quickMessageButtonText = 'Send';
$scope.quickMessage = {
name: '',
email: '',
content: '',
};
function resetFormData() {
$('#name').val('');
$('#email').val('');
$('#content').val('');
}
$scope.postQuickMessageToGoogle = _.throttle(function() {
setSubmittingIndicators();
MyNewService.sendQuickMessage(
$scope.quickMessage.name,
$scope.quickMessage.email,
$scope.quickMessage.content
)
.success(
//sucess Message
//can be as well a function that returns a status code
)
.error(
//error Message
);
}, 500);
}]);
Service
'use strict';
angular.module('myApp')
.factory('MyNewService', ['$http', function ($http) {
var myService = {};
myService.sendQuickMessage = function(name, email, content) {
$http({
method: 'JSONP',
url: 'https://docs.google.com/forms/d/MyFormKey/formResponse?'+
'entry.3=' + name +
'&entry.1=' + email +
'&entry.0=' + content
});
};
return myService;
}]);
Unit-test
'use strict';
describe('Controller: QuickMessageCtrl', function() {
var $httpBackend, $rootScope, $controller, scope, apiUrl;
beforeEach(module('myApp'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
apiUrl = $injector.get('apiUrl');
$httpBackend.expectJSONP(
'https://docs.google.com/forms/d/MyFormKey/formResponse?'+
'entry.3=test'+
'&entry.1=test#test.com'+
'&entry.0=thisIsContent'
).respond(200, {});
$rootScope = $injector.get('$rootScope');
scope = $rootScope.$new();
$controller = $injector.get('$controller');
$controller('QuickMessageCtrl', { $scope: scope });
}));
describe('form submit', function() {
var changeStateSpy;
beforeEach(function() {
scope.quickMessage.name = 'test';
scope.quickMessage.content = 'thisIsContent';
scope.quickMessage.email ='test#test.com';
});
afterEach(function(){
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should set submitting indicators on submit', function() {
scope.postQuickMessageToGoogle();
expect(scope.quickMessageButtonText).toBe('');
$httpBackend.flush();
});
});
});