I'm trying to test an Angular service with Karma, and was getting some strange results, so just for kicks I made two tests that were identical to see if I would get the same results, but I didn't:
describe('Profile Data Service', function() {
var LoginData, rootScope, $httpBackend;
beforeEach(module('myApp'));
describe('get profile data', function() {
beforeEach(inject(function(_LoginData_, $rootScope, _$httpBackend_) {
LoginData = _LoginData_;
LoginData.username= "jsmith";
LoginData.token = "token";
rootScope = $rootScope;
$httpBackend = _$httpBackend_;
}));
it('should get profile data on instantiation', inject(function(ProfileDataService) {
$httpBackend.expect('POST', 'http://mytestserver.com/api/', {
params: [{
username: "jsmith",
token: "token",
method: "getProfile"
}]
})
.respond({
result: {
address: "111 Main St."
city: "Los Angeles",
state: "CA"
}
});
$httpBackend.flush();
expect(ProfileDataService.profileData.address).toMatch("111 Main St.");
}));
it('should get profile data on instantiation', inject(function(ProfileDataService) {
$httpBackend.expect('POST', 'http://mytestserver.com/api/', {
params: [{
username: "jsmith",
token: "token",
method: "getProfile"
}]
})
.respond({
result: {
address: "111 Main St."
city: "Los Angeles",
state: "CA"
}
});
$httpBackend.flush();
expect(ProfileDataService.profileData.address).toMatch("111 Main St.");
}));
});
});
The first test passes, but the second test states that profileData is undefined. They are identical tests. I am assuming that for each it that the ProfileDataService is being re-initialized, but that may not be the case. If that's not true, how do I create separate tests so that my service is destroyed and re-initialized for each test case?
The logic for the service is fairly straightforward:
(function() {
'use strict';
angular.module('servicesModule')
.service('ProfileDataService', ProfileDataService);
ProfileDataService.$inject = ["$http", "$q", "LoginData", "CacheFactory"];
function ProfileDataService($http, $q, LoginData, CacheFactory) {
var ProfileDataService = this;
(function() {
init();
})();
function init() {
getProfile().then(function(profileData) {
ProileDataService.profileData = profileData;
});
}
function getProfile() {
var deferred = $q.defer();
var data = {
params: [{
username: LoginData.username,
token: LoginData.token,
method: "getProfile"
}]
};
var profileDataCache = CacheFactory.get('profileDataCache');
if (profileDataCache.get('profileData') && !invalidateCache) {
deferred.resolve(profileDataCache.get('profileData'));
}
else {
$http.post('http://mytestserver.com/api/', data)
.success(function(data) {
profileDataCache.put('profileData', response.result);
deferred.resolve(data.result);
});
}
return deferred.promise;
}
}
})();
I should also note that I've tried adding rootScope.$digest() in different places in the test, but that doesn't seem to make any difference. I thought manually triggering the digest cycle would ensure that the http post was caught and the response mocked.
Edit: I left out a huge detail... angular-cache. I forgot to mention that I was using a cacheing plugin. This was the reason for my problem. After creating a plunker and seeing that I was getting consistent results for identical tests, I knew there was something I was failing to realize. The second test was not making a POST request because of my cacheing logic.
My issue was failing to realize that my use of a cache was giving me different results for each test. Specifically, my service logic prevented a second POST to the api if a cache had already been persisted for that response.
Related
let me present first the scenario.
this is my angular code.
//this is my controller
function viewUserProfile($scope, $http, svcUserInfo) {
svcUserInfo.userInfo(null, function(data) {
$scope.profile = loadProfile(data);
}, function() {
Console.log('Error getting User info.');
});
}
so basically i am just loading a profile object on $scope.profile (e.g first name, last name,etc) using svcUserInfo REST service.
the service is created like this:
anythingHere.factory('svcUserInfo', ['$resource', function($resource) {
return $resource(SVC_USER, null, {
userInfo: {method: 'GET'}
});
}]);
now this one works perfectly so just assume other declarations or initialization before and after those codes.
now i am writing test codes using jasmine-maven-plugin here
what i want to achieve is to mock the service with and populate $scope.profile. and this is what i have so far:
describe('User Profile Controller', function() {
var rootScope, scope,
location = {}, route = {}, http = {}, userProfCtrl,
mockSvcUserInfo, userInfoPromise;
beforeEach(module('myApp'));
beforeEach(module(MOD_USERPROF));
beforeEach(module(function($provide){
$provide.factory('svcUserInfo', ['$q', function($q) {
var profile = {
nameType: 'Personal Name',
firstName: 'Joseph',
lastName: 'Bada',
email: 'email#email.com',
hasReferrer: false,
referrer: null,
country: 'myCountry',
state: 'myState',
city: 'myCIty',
address1: 'this is address1',
address2: 'this is address2',
zipcode: '9999',
phone: '+191234567',
maxProduct: '2'
};
function userInfo(data){
if(userInfoPromise){
return $q.when(profile);
} else {
return $q.reject();
}
}
return{
userInfo: userInfo
};
}]);
}));
beforeEach(inject(function($rootScope, $controller, svcUserInfo){
scope = $rootScope.$new();
rootScope = $rootScope.$new();
mockSvcUserInfo = svcUserInfo;
spyOn(mockSvcUserInfo, 'userInfo').and.callThrough();
userProfCtrl = $controller(CTRL_USERPROF, {
$scope: scope,
svcUserInfo: mockSvcUserInfo,
$rootScope: rootScope
});
}));
it('should call the rest service for User Profile and populate profile', function() {
userInfoPromise = true;
scope.$digest();
rootScope.$digest();
expect(mockSvcUserInfo.userInfo).toHaveBeenCalled();
expect(scope.profile).not.toBe(undefined); // or undefined anything like these
it("profile object: " + JSON.stringify(scope.profile)); // this is for debugging purposes so that i can check if scope.profile has values or not.
});
});
now on the spec i expect that after mockSvcUserInfo.user have been called.
then scope.profile must be populated/initialized since base on my code on the controller is that on successful call of the service, $scope.profile must be populated.
the result of this so far is that scope.profile goes undefined
i based my test codes from here:
http://www.sitepoint.com/mocking-dependencies-angularjs-tests/#mocking-methods-returning-promises
please help me, thanks
EDIT
I have updated the code coming from the first answer
it('should call the rest service for User Profile', inject(function($rootScope,$controller,svcUserInfo) {
scope = $rootScope.$new();
rootScope = $rootScope.$new();
userInfoPromise = true;
mockSvcUserInfo = svcUserInfo;
spyOn(mockSvcUserInfo, 'userInfo').and.callThrough();
userProfCtrl = $controller(CTRL_USERPROF, {
$scope: scope,
svcUserInfo: mockSvcUserInfo,
$rootScope: rootScope
});
scope.$digest();
rootScope.$digest();
expect(mockSvcUserInfo.userInfo).toHaveBeenCalled();
expect(scope.profile).not.toBe([]);
it("hello" + JSON.stringify(scope.profile));
}));
still scope.profile is undefined
I think i know the problem.
You set userInfoPromise to true in your test, but your controller is created in the setup process, and by the time you set the boolean value the service call is done.
Either set userInfoPromise inside beforeeach or move the controller creation itno the test.
I am new to AngularJS. We are using the karma/jasmine testing tool.
I am going to past the code I am trying to unit test here. I hope you can help me write a unit test. I am able to write very simple unit tests. But modules where there are a lot dependencies are giving me trouble.
Here is the code I am trying to unit test:
var referencedataservice = angular.module('referenceDataService', []);
referencedataservice.factory('dataService', function($localStorage, $http, constant) {
var username = $localStorage.user.username;
var password = $localStorage.user.password;
var request = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: {
username: username,
password: password
}
};
function getCountries() {
request.url = constant.URL_GET_COUNTRIES;
return $http(request).then(
function(result) {
return result.data;
});
}
}
Here is the what the constants service looks like:
angular.module('constantsService', []).constant("constant", {
"URL_GET_COUNTRIES": "app/lookup/getCountries",
"URL_GET_PRODUCT_TYPES": "app/lookup/getProductTypes",
});
I can unit test the constantsService quite easily. But I am unable to figure out how to write the unit test for my referenceDataService. Here is my attempt at it:
describe("Test Reference Data Service", function() {
var a;
var dataService;
var $localStorage;
var constant;
var $http;
beforeEach(module('ngStorage', 'constantsService', 'referenceDataService'));
beforeEach(inject(function($localStorage, _constant_, _dataService_) {
constant = _constant_;
$http = _$http_;
dataService = _dataService_;
}));
it("should be the same", function() {
var countries;
dataService.getCountries().then(function(data) {
countries = data.referenceDataValues;
});
a = countries[0].id;
expect(a).toBe("1000000");
});
});
It obviously doesn't work. The tests don't even compile. Keep getting dependancy related errors. I just don't know how to fix them.
Your getCountries is async so you need to use the "done" in your test.
it("should be the same", function(done) {
var countries;
dataService.getCountries().then(function(data) {
countries = data.referenceDataValues;
a = countries[0].id;
expect(a).toBe("1000000");
done();
});
});
Source: http://jasmine.github.io/2.0/introduction.html
This is my Controller:
rcCtrls.controller('LoginController', [
'$scope',
'AuthService',
function($scope, AuthService)
{
console.log('LoginController');
$scope.credantials = {
email: '',
password: '',
remember: false
}
$scope.login = function(credantials)
{
console.log('before login');
AuthService.login(credantials.email, credantials.password, credantials.remember || false).success(function(response) {
console.log('login successful');
console.log(response);
location.reload();
}).error(function() {
console.log('failed to login');
});
};
}]);
This is my AuthService:
rcServices.factory('AuthService', [
'$http',
function($http)
{
return {
login: function(email, password, remember) {
console.log('Auth Service Login');
return $http.post('/auth/login', {email: email, password: password, remember: remember});
},
logout: function() {
return $http.post('/auth/logout');
}
};
}]);
This is my spec:
describe('Controller: LoginController', function() {
// We initiate the app moudle mock
beforeEach(module('app')); // alias for angular.mock.module('app')
beforeEach(inject(function($controller, $rootScope, AuthService, $httpBackend){
this.$httpBackend = $httpBackend;
this.scope = $rootScope.$new();
$controller('LoginController', {
$scope: this.scope,
AuthService: AuthService
});
}));
describe("checking credantials at the begining", function() {
it("should be initiated", function() {
expect(this.scope.credantials).toEqual({ email: '', password: '', remember: false });
});
});
describe("successfully logged in", function() {
it("should redirect to home", function() {
this.scope.credantials = { email: 'test#mail.com', password: '123', remember: false };
this.$httpBackend.expectPOST('/auth/login', this.scope.credantials).respond(200, {test: 'test'});
this.scope.login(this.scope.credantials);
console.log(this);
this.$httpBackend.flush();
expect(this.scope).toBeDefined();
});
});
});
It seems as if the $httpBackend doesn't do what it is suppose to do.
I indeed get all the console logs up until the moment where my Service uses $http post request, and there it stops.
FYI, my controller is working properly on the app itself! The post is happening and the response is returning properly.
However the test just doesn't...
I'm getting and error:
Error: No pending request to flush !
Commenting it doesn't help.
EDIT:
The issue was my angular-mock.js version.... how sad.
In your service you call:
location.reload();
This could be messing with jasmine and causing unpredictable behavior. What does this call do, do you need it, and what happens to your tests if you just comment that out?
If you really need to make that call, you should use AngularJS built in $location service and mock it so that you keep the behavior but not break jasmine in the process.
===========
Update:
Additionally, it's a little odd that you're using this.httpBackend, when normally you'd totally overwrite the reference - not storing it as an attribute of this but a variable global to your tests. I am suspicious that somewhere else in your app you have called flush already and the reference isn't being reset properly. Do you have any other tests calling flush, anywhere?
http://plnkr.co/edit/jBewavvO1n8NBa6WGFja
I made working tests in this plunkr, which pass with your code as it is. With the exception of the location.reload() line which I commented out, which did not break the tests, although this test runner is a bit different as its in a plunkr and that won't always be the case. You really should not be calling that in tests.
Im new to jasmine/karma, trying to write a test for my angular application and i have a problem that i can't solve, hopefully someone here can help me.
My problem is that my login() function updates a value inside my controller but jasmine fails to see the updated value when it's inside .then() and the test fails, but when i update the value outside of the .then() it passes successfully.
here is my controller:
var Authctrl = this;
Authctrl.myVariable = "oldValue";
Authctrl.login = function () {
AuthService.login(Authctrl.credentials).then(function(authData){
Authctrl.credentials = { email: '',password: ''};
/*case 1 */ Authctrl.myVariable = "newValue"; //test gives error
},function(error){
console.log(error);
Authctrl.errors.login = 'Wrong username or password. Please try again';
});
/*case 2 */ Authctrl.myVariable = "newValue"; //test passes successfully
};
and my test code:
it('should be newValue',function(){
Authctrl.credentials = {
email: 'myEmail#yahoo.com',
password: '12345'
};
Authctrl.login();
expect(Authctrl.myVariable).toBe('newValue');
});
and my service:
authService.login = function (credentials) {
return $q(function(resolve, reject){
ref.authWithPassword(credentials , function(error, authData) {
if (error === null) {
// user authenticated with Firebase
console.log('SERVICE IS RUNNING, success'); //this does not log when testing with karma
resolve(authData);
} else {
console.log('SERVICE IS RUNNING, error'); //this does not log when testing with karma
reject(error);
}
},{
remember: "default"
});//ref.authWithPassword end
console.log('SERVICE IS RUNNING'); //this logs when testing with karma
});//$q end
};//authService.login end
so I finally figured this out and decided to post the answer here in case others run into the same problem. as #MatthewGreen mentioned, i had to create a mock service. and use the $provide to define mock AuthService methods and their return values. i followed many tutorials online and i kept getting errors, then i learnt that few things have changed in new jasmine and one of them is the spyOn command. This tutorial helped me a lot.
'use strict';
describe('Controller: AuthCtrl', function () {
var $rootScope,$scope,$controller,AuthService,AuthCtrl;
//fake firebase user data
var mockAuthData = {
provider: 'password',
password:{
email: 'myEmail#yahoo.com',
isTemporaryPassword: false
},
auth:{
provider:'password',
uid:'simplelogin:1'
},
uid:'simplelogin:1'
};
beforeEach(function() {
module('myApp');
// Provide will help us create fake implementations for our dependencies
module(function($provide) {
// Fake AuthService Implementation returning a promise
$provide.value('AuthService', {
login:function(){
return{
then:function(callback){return callback(mockAuthData);}
};
}
});
return null;
});
});
// load the controller's module
beforeEach(inject(function(_$rootScope_, _$controller_, _$q_, _AuthService_) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$controller = _$controller_;
AuthService = _AuthService_;
AuthCtrl = $controller('AuthCtrl',
{'$rootScope' : $rootScope, '$scope': $scope, 'AuthService': AuthService});
$rootScope.$apply();
}));
it("myVariable should be newValue", function() {
spyOn(AuthService, 'login').and.callThrough();
AuthCtrl.login();
expect(AuthService.login).toHaveBeenCalled();
expect(AuthCtrl.myVariable).toBe('newValue');
});
it("should retrieve the email address", function() {
spyOn(AuthService, 'login').and.callThrough();
AuthCtrl.login();
expect(AuthService.login).toHaveBeenCalled();
expect(AuthCtrl.userEmail).toBe('myEmail#yahoo.com');
});;
});
I have seen some questions regarding this but all of them was specific to each case and I couldn't find a solution for my case in those posts.
I have a current controller:
function Login(authService, $scope) {
var vm = this;
vm.submit = submit;
vm.form = {};
function submit() {
if ($scope.loginForm.$invalid) {
vm.invalid = true;
return;
} else {
var data = {
usr: vm.form.email,
pwd: vm.form.password,
vendorId: 99
};
authService.login(data).then(success, error);
}
}
function success(res) {
if (res.data) {
//Do stuff
}
}
function error(error) {
console.log("Error ", error);
}
}
And the following unit test:
describe('Login', function() {
beforeEach(module('app'));
var loginCtrl, scope, $httpBackend, authService;
var loginResponse = [{
"data": {
"avatar": "avatar",
"gender": "M",
"hid": "hid,
"id": "id",
"role": "Adult",
"token": "token"
}
}];
var loginRequest = { "usr": "test#teste.com", "pwd": "123teste!", "vendorId": 99 };
beforeEach(inject(function($rootScope, _$httpBackend_, $controller, _authService_) {
$httpBackend = _$httpBackend_;
scope = $rootScope.$new();
loginCtrl = $controller('Login', {
$scope: scope
});
authService = _authService_;
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe("submit", function() {
it("should send login data to the server", function() {
// expect(loginCtrl.login).toBe(false);
//Tells the $httpBackend service to expect a POST call to be made to a service and that it will return
//loginResponse object that was defined before
$httpBackend.expectPOST('api/current/api/login').respond(loginResponse);
//Execution of the service
var deferred = authService.login(loginRequest);
var users;
deferred.then(function(response){
users = response.data;
});
// expect(loginCtrl.login).toBe(true);
//Preserve the asynchronous nature of the call while at the same time be able to test the response of the call
$httpBackend.flush();
// dump(users);
expect(users).toEqual(loginResponse);
// expect(loginCtrl.login).toBe(true);
});
});
});
And I am getting the error:
Error: Unexpected request: GET signup/signup.html
No more request expected
I have found why this error occurs (I think). I'm using ui-router and it seems that it is always trying to do a GET request for the router root:
$urlRouterProvider.otherwise('/signup/');
$stateProvider
/* NOT AUTHENTICATED STATES */
.state('signup', {
url: '/signup/',
templateUrl: 'signup/signup.html',
controller: 'Signup as signupCtrl',
data: {
authorizedRoles: [AUTH_EVENTS.notAuthenticated]
}
})
Now I have no idea why, or how to fix it... Can someone understand what I'm doing wrong?
Thanks in advance!
Edit: authService
function authService($http, Session) {
var service = {
login : login
};
return service;
function login(credentials) {
console.log('authservice:', credentials);
return $http.post('api/current/api/login', credentials).then(function(res){
if (res.data.data){
var user = res.data.data;
Session.create(user.id, user.hid, user.token, credentials.usr, user.role, user.gender);
}
return res.data;
});
}
}
The template is requested, but you didn't inform the $http mock that it would be.
Register your template with $httpBackend
$httpBackend.expect('GET', 'signup/signup.html');
https://docs.angularjs.org/api/ngMock/service/$httpBackend#expect
Updated your function and try this code should work. Another option is to add ur template in template cache using gulp if you are using gulp this template get call problem is very specific with ui router and i have seen bug posted against it in github.
$httpBackend.expectGET('signup/signup.html').respond(200);
$httpBackend.flush();
$httpBackend.expectPOST('api/current/api/login').respond(loginResponse);
//Execution of the service
var deferred = authService.login(loginRequest);
var users;
deferred.then(function(response){
users = response.data;
});
// expect(loginCtrl.login).toBe(true);
//Preserve the asynchronous nature of the call while at the same time be able to test the response of the call
$httpBackend.flush();
// dump(users);
expect(users).toEqual(loginResponse);
// expect(loginCtrl.login).toBe(true);