I'm trying to test a controller in my Angular framework:
.controller('UserCtrl', ['$scope', '$location', 'User', 'Config',
function ($scope, $location, User, Config) {
...
}])
This controller depends on a couple of Services that require the $http object to make server calls:
.factory('User', ['$http', function ($http) {
var data = {};
return {
query: function(oper, putdata, callback){
if(oper == 'get'){
$http.get(getUrl("user",null)).success(function(data2) {
console.log(data2);
callback(data2);
data.userinfo = data2;
});
},
userinfo: data
};
}])
But when I try bootstrapping the controller I can't get httpBackend to work:
describe('UserCtrl', function(){
var ctrlScope, ctrl, $httpBackend, controllerService;
beforeEach(
inject(function($httpBackend, $http, $rootScope, $controller, User, Config) {
_User = User;
_Config = Config;
spyOn(User, 'getUserInfo').andCallThrough();
//spyOn(User, 'query').andCallThrough();
ctrlScope = $rootScope.$new();
controllerService = $controller;
httpMock = $httpBackend;
})
);
it('should create setup userinfo object ', function() {
httpMock.expectGET("/user/default/details").
respond({somejson});
ctrl = controllerService('UserCtrl', {$scope: ctrlScope, $location: location, User: _User, Config: _Config});
expect(_User.getUserInfo).toHaveBeenCalled();
httpMock.flush();
expect(ctrlScope.userinfo.length).toBe(1);
});
});
All I ever get is:
Error: No pending request to flush !
so is it possible to use httpBackend with a service that you have called from a controller you are testing?
Is there a reason you want to specifically test for a GET request? This is an implementation detail of your User service.
Regardless of whether your function is called query() or getUserInfo(), I suspect all that you really want to know is that the function was called on your User service. It's up to the User service to have it's own set of tests that will test that a GET request was made to /user/defaults/details.
The reasoning behind this is that you don't want unrelated unit tests to break when you change the implementation of your User service. What happens when your API changes to /user/query? Your UserCtrl tests should not break, nor should any other controller that's using User service. If your UserCtrl is calling getUserInfo() on User service, it's doing the right thing as far as you're concerned. Your User service unit tests will break and once you fix them to point to your new URL, everything is back in order.
Related
I'm testing out on of my controllers. I keep getting
Error: Expected POST /auth/signup with different data
EXPECTED: {"username":"justin","firstName":"Justin","lastName":"Young","email":"xxxx#xxx.com","company":"5579d602ba9f26a414be5d57","url":"http://www.me.com","referrer":"me#me.com"}
It's completing the post to auth/signup as expected, but the data is empty? I'm passing in sampleUserResponse into the expectations so I don't get why it's not passing that data back as the response. What am I doing wrong?
My test:
'use strict';
(function() {
// Authentication controller Spec
describe('AdminItemController', function() {
// Initialize global variables
var controller,
scope,
$httpBackend,
$stateParams,
$location;
beforeEach(function() {
jasmine.addMatchers({
toEqualData: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
return {
pass: angular.equals(actual, expected)
};
}
};
}
});
});
// Load the main application module
beforeEach(module(ApplicationConfiguration.applicationModuleName));
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
// This allows us to inject a service but then attach it to a variable
// with the same name as the service.
beforeEach(inject(function($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_) {
// Set a new global scope
scope = $rootScope.$new();
// Point global variables to injected services
$stateParams = _$stateParams_;
$httpBackend = _$httpBackend_;
$location = _$location_;
// Initialize the Authentication controller
controller = $controller('AdminItemController', {
$scope: scope,
$stateParams:$stateParams
});
$httpBackend.when('GET', 'modules/core/views/home.client.view.html').respond({});
}));
it('$scope.create() with valid form data should send a POST request with the form input values and then locate to new object URL', inject(function(Users) {
// Create a sample article object
var sampleUserPostData = new Users({
username: 'justin',
firstName:'Justin',
lastName:'Young',
email:'xxxx#xxx.com',
company:'5579d602ba9f26a414be5d57',
url:'http://www.me.com',
referrer:'me#me.com'
});
// Create a sample User response
var sampleUserResponse = new Users({
_id:'4579d602ba9f26a414be5d59',
username: 'justin',
firstName:'Justin',
lastName:'Young',
email:'xxxx#xxx.com',
company:'5579d602ba9f26a414be5d57',
url:'http://www.me.com',
referrer:'me#me.com'
});
// Fixture mock form input values
//scope.title = 'An User about MEAN';
//scope.content = 'MEAN rocks!';
// Set POST response
$httpBackend.expectPOST('/auth/signup', sampleUserPostData).respond(sampleUserResponse);
// Run controller functionality
scope.addPost();
$httpBackend.flush();
// Test form inputs are reset
//expect(scope.title).toEqual('');
//expect(scope.content).toEqual('');
// Test URL redirection after the User was created
//expect($location.path()).toBe('/admin/users/' + sampleUserResponse._id);
}));
});
}());
My Simplified Controller:
.controller('AdminItemController', ['$scope', '$http', '$location','apiResource','$stateParams', '$state','$log',
function($scope, $http, $location, apiResource, $stateParams, $state, $log) {
$scope.addPost = function() {
apiResource.save({api_resource:'auth', api_action: 'signup'},$scope.item).$promise.then(function(response){
$scope.$parent.users.push($scope.item);
});
};
}
])
The problem is in your controller, the params you send with the POST is $scope.item but in your test, you DO NOT set your $scope.item to be anything. Therefore, a POST with undefined params will be sent (because $scope.item is undefined). Moreover, in your test, you expect the params sent to equal to sampleUserPostData. Apparently it will fail because undefined !== sampleUserPostData. What you can do is just to set the scope.item = sampleUserPostData; before expectPOST and it will be fine.
Working fiddle.
I have the following test for a service object and the promise doesn't return and neither does the http request get called from inside the service, but it works in browser testing.
'use strict';
describe('Service: AuthService', function () {
// load the controller's module
beforeEach(module('adminPanelAngularApp'));
var AuthService, AuthService, $rootScope;
// Initialize the controller and a mock scope
beforeEach(inject(function (_AuthService_, _$rootScope_) {
AuthService = _AuthService_;
$rootScope = _$rootScope_;
}));
it('it auths', function () {
AuthService.login(SOMECREDENTIALS).then(function(){
console.log('this doesnt output in log');
});
expect(3).toBe(3);
});
});
this is my service
angular.module('adminPanelAngularApp').factory('AuthService', ['$http', '$cookieStore', '$rootScope', '$location', '$q', function ($http, $cookieStore, $rootScope, $location, $q) {
var authService = {};
....
authService.get_current_user = function(){
return $rootScope.current_user;
}
authService.login = function (credentials) {
var url = REDACTED;
return $http.post(server+url).then(function (res) {
if (!res.data){
return false;
}
if (res.data.error){
$rootScope.login_error = res.data.error;
}
var user = {
email: res.data.email,
session: res.data.session,
uid: res.data.uid
}
$cookieStore.put('loginData', user);
$rootScope.current_user = user;
return user;
});
};
...
what am I doing wrong with the tests?
I know my code is pretty bad too, but if I can test this then i'm halfway there.
If you don't want to mock $http, I suggest you to use $httpBackend.
With $httpBackend you can mock the calls you make with $http.
Imagine this service:
app.factory('Auth', function($http) {
return {
login: function() {
return $http.post('/login');
}
};
});
The goal is to test that you make your $http.post and it returns successfully, so the idea is like:
describe('Service: Auth', function() {
var Auth, $httpBackend;
beforeEach(function() {
module('app');
inject(function(_Auth_, _$httpBackend_) {
Auth = _Auth_;
$httpBackend = _$httpBackend_;
$httpBackend.whenPOST('/login').respond(200);
});
});
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should do a proper login', function() {
var foo;
Auth.login().then(function() {
foo = "success";
});
$httpBackend.flush();
expect(foo).toBe("success");
});
});
So, for starters, we inject what we need (Auth and $httpBackend)
And then, we call the whenPOST of $httpBackend. Basically it does something like:
When someone does a POST to /login, respond it with 200
Then on the test, we call login which is going to do the $http.post. To process this $http.post, since it is async, we can simulate the real call doing a $httpBackend.flush() which is going to "process" the call.
After that, we can verify that the .then was executed.
What about the afterEach? We don't really need it for this example, but when you want to assert yes or yes that a call was made, you can change the whenPOST to expectPOST, to make a test fail if that POST is never made. The afterEach is basically checking the status of the $httpBackend to see if any of those expectation weren't matched.
On the other hand, you don't need to create a promise by hand. $http returns a promise for you, so you can return the $http call directly, and on the $http then you can:
return user;
That will simplify the implementation a little bit.
Demo here
I've a basic function in a controller that calls out a service and was wondering how to go about testing this function.
controller
app.controller('loginCtrl', function($scope, loginService) {
$scope.login = function() {
loginService.login($scope);
}
});
service
app.factory('loginService', function(parserService, $location, $http {
return {
login : function(scope) {
parserService.get(function(data) {
if (scope.username === data.username
&& scope.password === data.password) {
authentication.isAuthenticated = true;
authentication.user = data.fullname;
authentication.userImg = data.imgUrl;
$location.url("/homepage");
} else {
scope.loginError = "Invalid login";
}
})
}
}
});
unit test so far
it('should call loginservice and check credentials', inject(function($rootScope,
$controller, loginService) {
//create a new scope that's a child of the $rootScope
$scope = $rootScope.$new();
$scope.login = function (){};
spyOn($scope, "login");
ctrl = $controller('loginCtrl', {
$scope : $scope,
loginService : loginService
});
$scope.username = 'admin';
$scope.password = 'a';
expected($scope.login).toHaveBeenCalledWith(loginService.login($scope);
///Some logic here to return successful login with give credentials/ or error `invalid login`
}));
});
Now how do I unit test this login service, do I need to modify my loginService.login() function login in order to be tested etc. Not sure how its done!!!
For testing a service that calls other services, you can usually mock those other services (or only the ones needed) and verify that the expected functions where called and the appropriate arguments where passed.
When $http or $resource are involved (which rely on $httpBackend) you can mock responses (or just verify certain endpoints where hit).
There is not enough info in your question regarding how $http, parseService or authenticate are used, but from what I can infer you could spy on $locations's url method or verify that $scope.loginError has been properly set.
See, also, this short demo.
I've a basic controller and inside I've a global function that is called from a different controller, which works absolutely fine in a running application.
Now does anybody know how to unit test this global function?
the cntrl it contains the function
apmstore.controller('HeaderCtrl', function(authentication, $rootScope, $scope, loginService){
// how to test this function called in the loginCtrl below
$rootScope.setDropDownStatus = function(visID, user, userImg) {
$scope.dropdown = visID;
$scope.user = user;
$scope.imgUrl = userImg;
};
});
loginCtrl
apmstore.controller('loginCtrl', function($scope, authentication, loginService,
$rootScope) {
authentication.isAuthenticated = false;
//Here is where its called, i want to unit test this
$rootScope.setDropDownStatus(false, null, null);
$scope.templates = [ {url : '/login'}, {url : '/config'} ];
$scope.login = function() {
loginService.login($scope);
}
});
Do I need to change the logic in HeaderCtrl to make it easier to test, i.e. decouple into a service/factory etc?
Anyone know how to tackle this? Thanks
Try this:
it('should call setDropDownStatus ', inject(function($rootScope, $controller, authentication, loginService) {
$scope = $rootScope.$new();
//fake the setDropDownStatus function as we don't care where this function is created
//We only care about whether this function is called.
$rootScope.setDropDownStatus = function(){
}
spyOn($rootScope, "setDropDownStatus");
////////
ctrl = $controller('loginCtrl', {
$scope: $scope,
authentication: authentication,
loginService: loginService,
$rootScope: $rootScope
});
//verify that this function is called with expected parameters.
expect($rootScope.setDropDownStatus).toHaveBeenCalledWith(false,null,null);
}));
DEMO
Yes to your last question. You shouldn't be defining a function in one controller and using it in another. Put it in a service and inject the service wherever needed.
I am new to AngularJS and have a service that loads my initial user configuration
angular.module('myApp').service('myService', ['$http', function ($http) {
var self = this;
self.user = {};
self.loadConfiguration = function () {
$http.get('/UserConfig').then(function (result) {
self.user = result.data;
});
};
self.loadConfiguration();
}]);
I have a controller that uses the configuration from this service
angular.module('myApp').controller('myController', ['$scope', 'myService', function ($scope, myService) {
var self = this;
// calculation based on service value
self.something = myService.user.something * something else;
}]);
The problem here is that myService.user.something may be undefined since the AJAX request may not have completed when this code is called. Is there a way to have the service complete before any other code is run? I want the service function 'loadConfiguration' to be run only once irrespective of the number of controllers that depend on it.
You can call your service method inside .run() function
Run Blocks
Run blocks are the closest thing in Angular to the main
method. A run block is the code which needs to run to kickstart the
application. It is executed after all of the service have been
configured and the injector has been created. Run blocks typically
contain code which is hard to unit-test, and for this reason should be
declared in isolated modules, so that they can be ignored in the
unit-tests.
https://docs.angularjs.org/guide/module
angular.module('myApp').run(function()){
//use your service here
}
One way to deal with ajax delay, is use $rootScope.$broadcast() function on $http.success which will broadcast your custom event to all controllers. Antoher way is to use promises and perform actions in controllers after resolve. Here are some ideas: https://groups.google.com/forum/#!topic/angular/qagzXXhS_VI/discussion
If you want to make sure that your code in controller gets executed after your AJAX call returns, you may use events.
Use this in your service:
angular.module('myApp').service('myService', ['$http', '$rootScope', function ($http, $rootScope) {
var self = this;
self.user = {};
self.loadConfiguration = function () {
$http.get('/UserConfig').then(function (result) {
self.user = result.data;
$rootScope.$broadcast('myService:getUserConfigSuccess');
});
};
self.loadConfiguration();
}]);
In your controller:
angular.module('myApp').controller('myController', ['$scope', 'myService', function ($scope, myService) {
var self = this;
$scope.$on('myService:getUserConfigSuccess', function() {
// calculation based on service value
self.something = myService.user.something * something else;
})
}]);
You can even attach an object to the event.
Please refer to https://docs.angularjs.org/api/ng/type/$rootScope.Scope .