I have generic service that create resources for my application:
(function(module) {
module.provider('restService', {
resourceRegistry: {},
addRestResource: function(entityName, entityProto) {
this.resourceRegistry[entityName] = entityProto;
},
$get: function() {
var restService;
for (var entityName in this.resourceRegistry) {
createRestResource(entityName, this.resourceRegistry[entityName]);
};
restService = {
//createRestResource: createRestResource
};
return restService;
}});
function createRestResource(entityName, entityProto) {
console.log('registering model: ' + entityName);
module.provider(entityName, { $get: function($resource, $http) {
var resource = $resource('/api/' + entityName + '/:id', { // TODO use config
id : '#id' //this binds the ID of the model to the URL param
},{
query : { method : 'GET', isArray : true }, //this can also be called index or all
update : { method : 'PUT' },
create : { method : 'POST' },
destroy : { method : 'DELETE' }
});
// here gose some other functionality unrelated to the topic...
return resource;
}});
}}(angular.module('restService', ['ngResource'])));
I can be used by any other module using
module.config(['restServiceProvider', function(restServiceProvider) {
restServiceProvider.addRestResource('User', { name: null, email: null });
}
And while above actually works for me in the application Actually above neither works for me in the application (it was working due to some code left from before refactoring) nor I cannot get working jasmine/karma test for it. The problem is that trying various method to configure restServiceProvider I always end with error stating that for eg TestEntity there is unknown provider TestEntityProider. Although I have tried different approaches to configure the resourceRegistry before the resource is being created, here is some test file.
describe('testing restService', function () {
var httpBackend;
var theRestServiceProvider;
beforeEach(function() {
module('ngResource');
module('restService');
var fakeModule = angular.module('test.app.config', ['ngResource'], function () {});
fakeModule.config( function (restServiceProvider) {
theRestServiceProvider = restServiceProvider;
restServiceProvider.addRestResource('TestModel', {testProp: null});
});
module('test.app.config');
});
beforeEach(function () {
inject(function ($httpBackend) {
httpBackend = $httpBackend;
})
});
beforeEach(inject(function (restService) {}));
describe('create restService entity', function() {
it('should post entity with nonempty testProp',
function() {
theRestServiceProvider.addRestResource('TestModel', {testProp: null});
inject(function(TestModel) {
var object = new TestModel();
object.testProp = 'John Doe';
httpBackend.expectPOST(/.*/,
function(postData) {
console.log("post data: " + postData);
jsonData = JSON.parse(postData);
expect(jsonData.testProp).toBe(object.testProp);
return jsonData;
}).respond({});
var response = object.$create();
httpBackend.flush();
});
});
});
});
IMO this is because within the test the registering resource is being done 'too late' but still I don't know how to do this right.
EDIT here is final resolution shortcut:
(function(module) {
module.provider('restService', function($provide) {
var provider = {};
// all the provider stuff goes here
function createRestResource(entityName, entityProto) {
/*
* using $provider here is fundamental here to properly create
* 'entityName' + Provider in the runtime instead of module initialisation
* block
*/
$provide.factory(entityName, function($resource, $http) { /* ... */ };
// do other stuff...
}
return provider;
}
}(angular.module('restServiceModule', ['ngResource'])))
I've made very similar thing here https://github.com/tunguski/matsuo-ng-resource/blob/master/matsuo-ng-resource.js, maybe it will help you.
Basically I'm adding new resource providers to $provide not to module. It works. I think it's main difference.
I am not very familiar with karma/jasmine but BDD syntax seems to be similar.
In my understanding you should have something like this
beforeEach(function () {
module('restService');
inject(function (_$httpBackend_, _restService_, _ngResource_) {
$httpBackend = _$httpBackend_;
restService = _restService_;
ngResource = _ngResource_;
})
});
instead of all you beforeEach. Read more here.
If you want to provide mocks instead of injected services you would use
module('restService', function($provide){
$provide.value('serviceToMock', mockObject);
});
Note that naming module and provider/service/factory with same name may be confusing at later stage...
Related
I'm new at angular and at unit testing with angular. We are using odata for database CRUD actions, so we have created a service for that, looks like this:
function DatabaseService($http, $odataresource, DateFactory, constants) {
var url = constants.BACKEND.URL;
var ObjCreate = $odataresource(url + 'Objects/Function.CreateObject', {}, {}, {});
var service = {
createSomething: {
createObj: createObj
}};
return service;
function createObj(formData) {
var myObj = new ObjCreate();
mapData(formData, myObj );
return myObj.$save();
}
The code is a bit abstracted for my question, so don't wonder please. I want to unit test the function createObj() now, which doesn't work. I took an angular class and we learned there that for 'execute' promises we have to use $rootScope.digest(), but it doesn't seem to work in my case:
describe('createObj', function () {
it('should return data', inject(function ($rootScope) {
var DatabaseService = $injector.get('DatabaseService', { $odataresource: $odataresource });
var formDataMock = {
productName: "Produktname"
};
var test = 'abc';
DatabaseService.createSomething.createObj(formDataMock)
.then(function (data) {
test = data;
})
.catch(function (error) {
test = error;
});
$rootScope.$digest();
console.log(test);
}));
I have added the setting of the variable test to see when for example the then path is executed, but even with the $rootScope.$digest it will never step into the then path, my variable test will never change from 'abc' to something else.
Could you please give me a hint what am I doing wrong?
I tried to update your code to use the done Feature of Jasmine 2.0.
http://ng-learn.org/2014/08/Testing_Promises_with_Jasmine/
describe('createObj', function () {
it('should return data', function (done) {
var DatabaseService = $injector.get('DatabaseService', { $odataresource: $odataresource });
var formDataMock = {
productName: "Produktname"
};
var test = 'abc';
DatabaseService.createSomething.createObj(formDataMock)
.then(function (data) {
test = data;
})
.catch(function (error) {
test = error;
})
.finally(done);;
console.log(test);
});
I'm testing this particular function:
function apiInjector($location, $cookies) {
var apiVersion = 1, baseUrl;
console.log('Host: '+$location.host());
if($location.host() == 'localhost') {
baseUrl = $cookies.get('API_HOST') || 'http://localhost:5000';
} else {
baseUrl = 'productionURL';
}
if(!baseUrl)
throw('Invalid host defined');
function isApiRequest(url) {
return url.substr(url.length - 5) !== '.html';
}
return {
request: function(config) {
if(isApiRequest(config.url)) {
config.url = baseUrl + '/' + apiVersion + '/' + config.url;
}
return config;
}
};
}
As you can see it makes use of $location.host to determine what the host is. I've created a mock to use so I can control the flow when it comes to the if-else statement:
var apiInjector, $location;
var mockedLocation = {
host: function() {
return 'localhost';
}
};
beforeEach(module('flowlens'), function($provide) {
$provide.value('$location', mockedLocation);
});
beforeEach(inject(function(_apiInjector_, _$location_) {
apiInjector = _apiInjector_;
$location = _$location_;
}));
describe('apiInjector',function(){
it('should be defined', function() {
expect(apiInjector).toBeDefined();
});
it('should expose a request function', function() {
expect(apiInjector.request).toBeDefined();
});
});
But when I call the function (apiInjector.request) I always see server printed when i insert a console.log ($location.host()) in the actual code (see above). What am I doing wrong here?
EDIT
This is what is printed when i print the result of apiInjector.request.
Object{url: 'productionURL/1/dashboard'}
But based on the code above and assuming that the host() function returns localhost (which it doesn't it returns server) it should print either the result of the $cookies.get or the http://localhost:5000
You need to load your module within the function passed to the beforeEach block then pass the function using $provide as a callback to module:
beforeEach(function() {
module('flowlens', function($provide) {
$provide.value('$location', mockedLocation);
});
});
Or you could write it like this:
beforeEach(module('flowlens'));
beforeEach(module(function($provide) {
$provide.value('$location', mockedLocation);
}));
Alternatively, sinon has some good options for spies, stubs, mocks, etc. I've used it to mock $location like:
var location,
locationStub;
beforeEach(inject(function($location) {
location = $location;
locationStub = sinon.stub(location, 'url');
locationStub.returns('some/fake/url');
}
I am starting to learn angularjs, so far i can create update delete withoud using services. I am trying to take it to the next level: Ive created a service page that looks like this:
app.factory('MainService', function($http) {
var getFeaturesFromServer = function() {
return $http.get('restfullfeatures/features');
}
var deleteFeature = function(id) {
return $http.post('restfullfeatures/delete', {'id': id});
}
var createFeature = function(feature) {
return $http.post('restfullfeatures/create', {'title': feature.title, 'description': feature.description});
}
return {
getHello : getHello,
getFeatures: getFeaturesFromServer,
deleteFeature: deleteFeature,
createFeature: createFeature
}
});
and my add function in controller looks like this:
$scope.add = function(){
MainService.createFeature($scope.formFeature)
.then(function(response) {
console.log('feature created',response.data);
$scope.features.push($scope.formFeature);
$scope.formFeature = {};
}, function(error) {
alert('error',error);
});
};
And this is my postCreate function:
public function postCreate()
{
\App\Feature::create(
['title' => \Input::get('title'),
'description' => \Input::get('description')
]);
return ['success' => true];
}
I have a table in my database called features, so basically what i am trying to do is add a new feature to my table using angularjs, my controller doesnt seem to recognize formFeature all i get is 'undefined' then i get the error: Cannot read property of type undefined, but I am using it in my delete function and it works perfectly, what did i miss here??
Factory
So when creating a factory for CRUD, try to lay out your factory like the example below. The example is some code I wrote for a project each call willl do a different thing, the idea is that you add a method that you can instantiate when you add the factory to your controller. (note: don't use $rootScope for session)
.factory('Chats', function ($http, $rootScope, $stateParams) {
return {
all: function () {
return $http.get('http://your_ip/chats', { params: { user_id: $rootScope.session } })
},
get: function () {
return $http.get('http://your_ip/chat', { params: { user_id: $rootScope.session, chat_id: $stateParams.idchat } })
},
add: function (id) {
return $http.post('http://your_ip/newChat', { params: {idfriends:id}})
}
};
});
Controller
when you instantiate this in your controller your looking at something like this
.controller('ChatsCtrl', function ($scope, Chats) {
Chats.all().success(function (response) {
$scope.chats = response;
});
$scope.getChat = function (id) {
Chats.get().success(function (response) { })
};
$scope.addChat = function (id) {
Chats.add(id).success(function (response) { })
};
})
$scope.remove and $scope.addChat and linked to buttons that will execute on click and $scope.chats is bound to the page via an ng-repeat.
Conclusion
Clean up your factories to look like this and you can easily write a series of reusable methods that are easy to instantiate in your controller.
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);
I'm trying to learn angular unit test with $resource.
Here I have a simple controller :
.controller('DictionaryCtrl', function ($scope, DictionaryService) {
$scope.jSearchDictionary = function () {
$scope.word = DictionaryService.getByJword({ jp: $scope.jword });
}
$scope.eSearchDictionary = function () {
$scope.word = DictionaryService.getByEword({ eng: $scope.eword });
}
})
In my view, I have 2 ng-submit (jSearchDictionary and eSearchDictionary) and i bind the corresponding word that is searched ( jword or eword ).
The service is also quite simple :
.factory('DictionaryService', function ($resource) {
return $resource('http://127.0.0.1:3000/api/nlp/words', {}, {
getByJword: { method: 'GET', params: { jp: '#jword' } },
getByEword: { method: 'GET', params: { en: '#eword' } },
})
})
Finally, here is my test.
describe('Controller: nlpCtrl', function () {
beforeEach(function () {
this.addMatchers({
toEqualData: function (expected) {
return angular.equals(this.actual, expected);
}
});
});
beforeEach(module('gakusei'));
describe('nlpCtrl', function () {
var scope,
$controller,
$httpBackend,
$stateParams,
Eword,
mockWord = [
{
"words": [
{
"readings": [
"ホッケー"
]
}
],
"count": 1
}];
beforeEach(inject(function (_$httpBackend_, $rootScope, _$controller_) {
scope = $rootScope.$new();
$controller = _$controller_;
$httpBackend = _$httpBackend_;
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should get a word', inject(function (DictionaryService) {
Eword = "englishWord";
$httpBackend.expectGET('http://127.0.0.1:3000/api/nlp/words?eng=englishWord')
.respond(mockWord[0]);
var ctrl = $controller('DictionaryCtrl', { $scope: scope });
var request = DictionaryService.getByEword({ eng: Eword })
$httpBackend.flush();
expect(scope.word).toEqualData(mockWord[0]);
expect(BasketService.getByJword).toBeTruthy();
expect(BasketService.getByEword).toBeTruthy();
}));
});
});
The problem is at the line :
expect(scope.word).toEqualData(mockWord[0]);
scope.word being undefined. Unit Testing is way over my head right now, I'm not sure of what I'm doing at all. If you have a solution to this particular problem, have any advices at all concerning all the code or are willing to message me and guide me a little, that would be awesome.
You have couple issues in your expectation and set up.
1) You are testing a controller and its scope, so do actions on the controller methods and set values on controller scope.
2) Instead of doing Eword = "englishWord"; you should set the value on the controller scope scope.eword = "englishWord";
3) Instead of calling service method directly DictionaryService.getByEword({ eng: Eword }) , you need to invoke the method on the scope, i.e scope.eSearchDictionary(); so that when the method is resolved it resolves with respective data and sets it on the scope.
4) Note that when you test against scope.word directly you may not get desired result since the result object will have additional properties like $promise on it. Since you are directly assigning the results.
5) I am not sure if you need the last 2 expectations at all.
Try:-
it('should get a word', inject(function (DictionaryService) {
scope.eword = "englishWord";
$httpBackend.expectGET('http://127.0.0.1:3000/api/nlp/words?eng=englishWord')
.respond(mockWord[0]);
$controller('DictionaryCtrl', { $scope: scope });
scope.eSearchDictionary();
$httpBackend.flush();
expect(scope.word.words[0]).toEqual(mockWord[0].words[0]);
/*I dont think you need the following expectations at all*/
expect(DictionaryService.getByJword).toBeDefined();
expect(DictionaryService.getByEword).toBeDefined();
}));
Plnkr
Some syntax of expectation utility method is different from what you are using, you can use the same that you use, i just did it for the demo
The variable you are looking for does not exist outside of those two functions. Try defining it at the top of your controller like so:
.controller('DictionaryCtrl', function ($scope, DictionaryService) {
$scope.word = '';
$scope.jSearchDictionary = function () {
$scope.word = DictionaryService.getByJword({ jp: $scope.jword });
}
$scope.eSearchDictionary = function () {
$scope.word = DictionaryService.getByEword({ eng: $scope.eword });
}
})