I have a function called getFrame in a service. The function just returns the $http call to controller.
angular.module('app').factory('DemoFactory', function ($http) {
function getFrame(id) {
var url = 'http://localhost:8080/frames/' + id + '/';
return $http.get(url);
}
return {
getFrame: getFrame
};
});
Now I want to write unittest for this which I am doing as follows:
describe('Service: DemoFactory', function () {
// load the service's module
beforeEach(module('app'));
// Instantiate service
var $httpBackend,
DemoFactory;
beforeEach(inject(function (_$httpBackend_, _DemoFactory_) {
$httpBackend = _$httpBackend_;
DemoFactory = _DemoFactory_;
}));
it('should send proper http request from getFrame', function () {
$httpBackend.expectGET('http://localhost:8080/frames/1/').respond(200);
DemoFactory.getFrame(1);
$httpBackend.flush();
});
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
});
With the given service my aim is to test whether getFrame is making a proper http request or not. So I think I am doing OK here. But something made me wonder that it block is not having any expect. So I need to confirm that for the service I have written I can have unit test as described. Do I need to have anything else in the unit test or can I do it any other way?
Related
I am working on an angular js app with karma/Jasmine testing framework, I need to test a factory that returns a http promise but it always return undefined
here is my factory
angular.module('GithubUsers').factory('Users',['$http','$q',function($http,$q){
return{
getAllUsers:function(){
var defered= $q.defer();
$http({
url:'https://api.github.com/users',
method:'GET'
}).then(function(users){
defered.resolve(users.data);
},function(err){
defered.reject(err);
})
return defered.promise;
}
}
}])
here is my tests
Update thanks to your answers I modified my code to the following but no I got this error
Possibly unhandled rejection: {"status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"jsonpCallbackParam":"callback","url":"https://api.github.com/users?since=1","headers":{"Accept":"application/json, text/plain, /"},"cached":false},"statusText":""} thrown
describe('Test Users Factory',function(){
var $controller,
Users,
$rootScope,
$httpBackend,
$q;
beforeEach(module('GithubUsers'));
beforeEach(inject(function(_$controller_,_Users_,_$rootScope_,_$httpBackend_,_$q_){
$controller = _$controller_;
Users = _Users_;
$rootScope= _$rootScope_;
$httpBackend=_$httpBackend_;
}))
it('should get users',function(){
var result;
$httpBackend.whenGET('https://api.github.com/users?since=1').respond(function(){
return {data:[{id:2}],status:200};
})
Users.getAllUsers().then(function(res){
result = res;
});
$httpBackend.flush();
$rootScope.$digest()
expect(result).toBeTruthy();
})
})
Thanks in advance!
I think you need to pass a function that returns a array with 3 items in it, to whenGET().respond().
Maybe, you can try something like this:
beforeEach(angular.mock.inject(function (User, $httpBackend, $http) {
...
this.withOKUsers = function() {
var i1 = new User();
i1.id = 10;
return [200, JSON.stringify([ i1]), {}];
} ...
}));
...
it('should get users',function(){
$httpBackend
.whenGET('https://api.github.com/users')
.respond(this.withOKUsers);
Users.getAllUsers().then(function(res){
result = res;
});
$httpBackend.flush();
expect(result).not.toBeNull();
...
(I prefer to arrange spec outside of it() clause for better readability)
You're missing a $httpBackend.flush(); call after your test method call. It will invoke a success/error or then part and resolve a $q's promise properly. For more tests I would move a $httpBackend.whenGET to each test case separately so I can later verify it per use case but it's just my personal opinion.
I find it a little suspicious that you mix a $controller and a factory in one test. I would suggest to split them, and in controller test just check the calls to service methods and in a facotry test itself do a $httpBackend stuff.
Below I paste your test with my corrections. It works now for me:
describe('Test Users Factory', function () {
var Users,
$rootScope,
$httpBackend,
$q;
beforeEach(module('app.utils'));
beforeEach(inject(function (_Users_, _$rootScope_, _$httpBackend_, _$q_) {
Users = _Users_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should get users', function () {
var result;
$httpBackend.when('GET', "https://api.github.com/users").respond({ data: [{ id: 2 }], status: 200 });
Users.getAllUsers().then(function (res) {
result = res;
expect(result).toBeTruthy();
});
$httpBackend.flush();
$rootScope.$digest();
});
Important notices:
1)afterEach - check if no pending requests remain after your call
2) your url differ with a parameter ?since=1. But you do not give it as a parameter in your code so i do not understand why you added this parameter.
Maybe consider string concatenation with url and parameter ?
I have a controller that calls a service and sets some variables. I want to test that those variables get set to the response.
My Controller:
tankService.getCurrentStats().success(function (response) {
$scope.stats.tankAvgHours = response.tankAvgHours;
$scope.stats.stillAvgHours = response.stillAvgHours;
$scope.stats.stillRemaining = response.stillRemaining;
$scope.stats.tankRemaining = response.tankRemaining;
$scope.stats.loaded = true;
});
My Test:
...
var STATS_RESPONSE_SUCCESS =
{
tankAvgHours:8,
stillAvgHours:2,
stillRemaining: 200,
tankRemaining:50
};
...
spyOn(tankService, "getCurrentStats").and.callThrough();
...
it('calls service and allocates stats with returned data', function () {
expect($scope.stats.loaded).toBeFalsy();
$httpBackend.whenPOST('../services/tanks/RelayTankService.asmx/getCurrentStats').respond(200, $q.when(STATS_RESPONSE_SUCCESS));
tankService.getCurrentStats()
.then(function(res){
result = res.data.$$state.value;
});
$httpBackend.flush();
expect($scope.stats.tankAvgHours).toEqual(result.tankAvgHours);
expect($scope.stats.stillAvgHours).toEqual(result.stillAvgHours);
expect($scope.stats.stillRemaining).toEqual(result.stillRemaining);
expect($scope.stats.tankRemaining).toEqual(result.tankRemaining);
expect($scope.stats.loaded).toBeTruthy();
});
The result is that my scope variables are undefined and don't equal my mocked response data. Is it possible to pass the mocked values so I can test the success function correctly populates the variables?
Thanks!
Since you're testing the controller, there's no need to mock $http's POST the way you've done in the test.
You just need to mock the tankService's getCurrentStats method.
Assuming that your tankService's getCurrentStats method returns a promise, this is how your test must be:
describe('controller: appCtrl', function() {
var $scope, tankService, appCtrl;
var response = {
tankAvgHours: 8,
stillAvgHours: 2,
stillRemaining: 200,
tankRemaining: 50
};
beforeEach(module('myApp'));
beforeEach(inject(function($controller, $rootScope, _tankService_) {
$scope = $rootScope.$new();
tankService = _tankService_;
spyOn(tankService, 'getCurrentStats').and.callFake(function() {
return {
then: function(successCallback) {
successCallback(response);
}
}
});
AdminController = $controller('appCtrl', {
$scope: $scope,
tankService: _tankService_
});
}));
describe('appCtrl initialization', function() {
it('calls service and allocates stats with returned data', function() {
expect(tankService.getCurrentStats).toHaveBeenCalled();
expect($scope.stats.tankAvgHours).toEqual(response.tankAvgHours);
expect($scope.stats.stillAvgHours).toEqual(response.stillAvgHours);
expect($scope.stats.stillRemaining).toEqual(response.stillRemaining);
expect($scope.stats.tankRemaining).toEqual(response.tankRemaining);
expect($scope.stats.loaded).toBeTruthy();
});
});
});
Hope this helps.
I think you're testing the wrong thing. A service should be responsible for returning the data only. If you want to test the service, then by all means mock the httpbackend and call the service, but then verify the data returned by the service, not the $scope. If you want to test that your controller calls the service and adds the data to the scope, then you need to create your controller in the test, give it scope that you create, and then test that those variables get added. I didn't test this so the syntax might be off, but this is probably the direction you want to go in.
var scope, $httpBackend, controller;
beforeEach(inject(function(_$httpBackend_, $controller, $rootScope) {
$httpBackend = _$httpBackend_;
$httpBackend.whenPOST('../services/tanks/RelayTankService.asmx/getCurrentStats').respond(200, $q.when(STATS_RESPONSE_SUCCESS));
scope = $rootScope.$new();
controller = $controller('myController', {
$scope: scope
});
}));
it('calls service and allocates stats with returned data', function () {
expect(scope.stats.loaded).toBeFalsy();
$httpBackend.flush();
expect(scope.stats.tankAvgHours).toEqual(result.tankAvgHours);
expect(scope.stats.stillAvgHours).toEqual(result.stillAvgHours);
expect(scope.stats.stillRemaining).toEqual(result.stillRemaining);
expect(scope.stats.tankRemaining).toEqual(result.tankRemaining);
expect(scope.stats.loaded).toBeTruthy();
});
I was following this example.
We have test suite like:
describe('Basic Test Suite', function(){
var DataService, httpBackend;
beforeEach(module('iorder'));
beforeEach(inject(
function (_DataService_, $httpBackend) {
DataService = _DataService_;
httpBackend = $httpBackend;
}
));
//And following test method:
it('should call data url ', function () {
var promise = DataService.getMyData();
promise.then(function(result) {
console.log(result, promise); // Don't gets here
}).finally(function (res) {
console.log(res); // And this is also missed
})
})
});
How to make jasmine + karma work with angular services, that returns promise?
I have seen this question, but looks like it's about using promises in test cases. Not about testing promises.
You need to tell jasmine that your test is asynchronous so that it waits for the promises to resolve. You do this by adding a done parameter to your test:
describe('Basic Test Suite', function(){
var DataService, httpBackend;
beforeEach(module('iorder'));
beforeEach(inject(
function (_DataService_, $httpBackend) {
DataService = _DataService_;
httpBackend = $httpBackend;
}
));
//And following test method:
it('should call data url ', function (done) {
var promise = DataService.getMyData();
promise.then(function(result) {
console.log(result, promise); // Don't gets here
done();//this is me telling jasmine that the test is ended
}).finally(function (res) {
console.log(res); // And this is also missed
//since done is only called in the `then` portion, the test will timeout if there was an error that by-passes the `then`
});
})
});
By adding done to the test method, you are letting jasmine know that it is an asynchronous test and it will wait until either done is called, or a timeout. I usually just put a call to done in my then and rely on a timeout to fail the test. Alternatively, I believe you can call done with some kind of error object which will also fail the test, so you could call it in the catch.
I am trying to test a real http call with Jasmine (integration test), but when i call a method that uses $http.get, it times out and the server never gets called.
I know that I am supposed to inject the implementation of $http but not sure where that should happen.
searchSvc
app.service('searchSvc', ['$http', '$q', searchSvc]);
function searchSvc($http, $q) {
return {
search: function(text) {
console.log('svc.search called with ', text); // this does get called
return $q.when($http.get('/search/' + text));
}
};
}
searchSpec
describe("searchTest", function() {
var ctrl, svc, $http;
beforeEach(function () {
module('testApp');
inject(function(_$controller_, searchSvc, _$http_){
ctrl = _$controller_('searchCtrl');
svc = searchSvc;
$http = _$http_;
})
});
it('test server search', function(done) {
svc.search('re').then(function(result) {
console.log('promise then'); // this never gets called, because server never gets called
expect(result).not.toBeNull();
expect(result.data).not.toBeNull();
expect(result.data.length).toBeGreaterThan(0);
done();
});
});
In case if you use promises you can find out how to deal with them here http://entwicklertagebuch.com/blog/2013/10/how-to-handle-angularjs-promises-in-jasmine-unit-tests/
This is sort of hypothetical, but if you include both ngMock & ngMockE2E modules as your app module's dependency (ngMock needs to come before ngMockE2E in the dependency list) you should be able to use $httpBackend service provided by ngMockE2E module to passThrough the search api call to actual backend in your test specs.
Try something like this and see whether it works:
describe("searchTest", function() {
var ctrl, svc, $httpBackend;
beforeEach(function () {
module('testApp');
inject(function(_$controller_, searchSvc, _$httpBackend_){
$httpBackend = _$httpBackend_;
ctrl = _$controller_('searchCtrl');
svc = searchSvc;
});
});
it('test server search', function(done) {
$httpBackend.whenGET(/^\/search\//).passThrough();
svc.search('re').then(function(result) {
console.log('promise then'); // this never gets called, because server never gets called
expect(result).not.toBeNull();
expect(result.data).not.toBeNull();
expect(result.data.length).toBeGreaterThan(0);
done();
});
});
});
Here is a solution that I use to make real HTTP calls when I'm using ngMock for unit tests. I mainly use it for debugging, working through the test, getting JSON examples etc.
I wrote a more detailed post about the solution on my blog: How to Unit Test with real HTTP calls using ngMockE2E & passThrough.
The solution is as follows:
angular.mock.http = {};
angular.mock.http.init = function() {
angular.module('ngMock', ['ng', 'ngMockE2E']).provider({
$exceptionHandler: angular.mock.$ExceptionHandlerProvider,
$log: angular.mock.$LogProvider,
$interval: angular.mock.$IntervalProvider,
$rootElement: angular.mock.$RootElementProvider
}).config(['$provide', function($provide) {
$provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
$provide.decorator('$$rAF', angular.mock.$RAFDecorator);
$provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
$provide.decorator('$rootScope', angular.mock.$RootScopeDecorator);
$provide.decorator('$controller', angular.mock.$ControllerDecorator);
}]);
};
angular.mock.http.reset = function() {
angular.module('ngMock', ['ng']).provider({
$browser: angular.mock.$BrowserProvider,
$exceptionHandler: angular.mock.$ExceptionHandlerProvider,
$log: angular.mock.$LogProvider,
$interval: angular.mock.$IntervalProvider,
$httpBackend: angular.mock.$HttpBackendProvider,
$rootElement: angular.mock.$RootElementProvider
}).config(['$provide', function($provide) {
$provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
$provide.decorator('$$rAF', angular.mock.$RAFDecorator);
$provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
$provide.decorator('$rootScope', angular.mock.$RootScopeDecorator);
$provide.decorator('$controller', angular.mock.$ControllerDecorator);
}]);
};
Include this source file after ngMock, for example:
<script type="text/javascript" src="angular.js"></script>
<script type="text/javascript" src="angular-mocks.js"></script>
<!-- this would be the source code just provided -->
<script type="text/javascript" src="ngMockHttp.js"></script>
How to write the test?
describe('http tests', function () {
beforeEach(module('moviesApp'));
var $controller;
var $httpBackend;
var $scope;
describe('real http tests', function() {
beforeEach(angular.mock.http.init);
afterEach(angular.mock.http.reset);
beforeEach(inject(function(_$controller_, _$httpBackend_) {
$controller = _$controller_;
$scope = {};
$httpBackend = _$httpBackend_;
// Note that this HTTP backend is ngMockE2E's, and will make a real HTTP request
$httpBackend.whenGET('http://www.omdbapi.com/?s=terminator').passThrough();
}));
it('should load default movies (with real http request)', function (done) {
var moviesController = $controller('MovieController', { $scope: $scope });
setTimeout(function() {
expect($scope.movies).not.toEqual([]);
done();
}, 1000);
});
});
});
How it works?
It uses ngMockE2E's version of $httpBackEndProvider, which provides us with the passThrough function we see being used in the test. This does as the name suggests and lets a native HTTP call pass through.
We need to re-define the ngMock module without its fake version of the $BrowserProvider, since that is what prevents the real HTTP calls in unit tests that use ngMock.
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