I have an AngularJS app set up with tests using Karma+Jasmine. I have a function I want to test that takes a large JSON object, converts it to a format that's more consumable by the rest of the app, and returns that converted object. That's it.
For my tests, I'd like you have separate JSON files (*.json) with mock JSON content only--no script. For the test, I'd like to be able to load the JSON file in and pump the object into the function I'm testing.
I know I can embed the JSON within a mock factory as described here: http://dailyjs.com/2013/05/16/angularjs-5/ but I really want the JSON to not be contained within script--just straight JSON files.
I've tried a few things but I'm fairly noob in this area. First, I set up my Karma to include my JSON file just to see what it would do:
files = [
...
'mock-data/**/*.json'
...
]
This resulted in:
Chrome 27.0 (Mac) ERROR
Uncaught SyntaxError: Unexpected token :
at /Users/aaron/p4workspace4/depot/sitecatalyst/branches/anomaly_detection/client/anomaly-detection/mock-data/two-metrics-with-anomalies.json:2
So then I changed it to just serve the files and not "include" them:
files = [
...
{ pattern: 'mock-data/**/*.json', included: false }
...
]
Now that they're only served, I thought I'd try to load in the file using $http from within my spec:
$http('mock-data/two-metrics-with-anomalies.json')
When I ran the spec I received:
Error: Unexpected request: GET mock-data/two-metrics-with-anomalies.json
Which in my understanding means it expects a mocked response from $httpBackend. So...at this point I didn't know how to load the file using Angular utilities so I thought I'd try jQuery to see if I could at least get that to work:
$.getJSON('mock-data/two-metrics-with-anomalies.json').done(function(data) {
console.log(data);
}).fail(function(response) {
console.log(response);
});
This results in:
Chrome 27.0 (Mac) LOG: { readyState: 4,
responseText: 'NOT FOUND',
status: 404,
statusText: 'Not Found' }
I inspect this request in Charles and it's making a request to
/mock-data/two-metrics-with-anomalies.json
Whereas the rest of the files I've configured to be "included" by Karma are being requested at, for example:
/base/src/app.js
Apparently Karma's setting up some sort of base directory to serve the files from. So for kicks I changed my jquery data request to
$.getJSON('base/mock-data/two-metrics-with-anomalies.json')...
And it works! But now I feel dirty and need to take a shower. Help me feel clean again.
I'm using an angular setup with angular seed. I ended up solving this with straight .json fixture files and jasmine-jquery.js. Others had alluded to this answer, but it took me a while to get all the pieces in the right place. I hope this helps someone else.
I have my json files in a folder /test/mock and my webapp is in /app.
my karma.conf.js has these entries (among others):
basePath: '../',
files: [
...
'test/vendor/jasmine-jquery.js',
'test/unit/**/*.js',
// fixtures
{pattern: 'test/mock/*.json', watched: true, served: true, included: false}
],
then my test file has:
describe('JobsCtrl', function(){
var $httpBackend, createController, scope;
beforeEach(inject(function ($injector, $rootScope, $controller) {
$httpBackend = $injector.get('$httpBackend');
jasmine.getJSONFixtures().fixturesPath='base/test/mock';
$httpBackend.whenGET('http://blahblahurl/resultset/').respond(
getJSONFixture('test_resultset_list.json')
);
scope = $rootScope.$new();
$controller('JobsCtrl', {'$scope': scope});
}));
it('should have some resultsets', function() {
$httpBackend.flush();
expect(scope.result_sets.length).toBe(59);
});
});
The real trick was the jasmine.getJSONFixtures().fixturesPath='base/test/mock';
I had originally set it to just test/mock but it needed the base in there.
Without the base, I got errors like this:
Error: JSONFixture could not be loaded: /test/mock/test_resultset_list.json (status: error, message: undefined)
at /Users/camd/gitspace/treeherder-ui/webapp/test/vendor/jasmine-jquery.js:295
Serving JSON via the fixture is the easiest but because of our setup we couldn't do that easily so I wrote an alternative helper function:
Repository
Install
$ bower install karma-read-json --save
OR
$ npm install karma-read-json --save-dev
OR
$ yarn add karma-read-json --dev
Usage
Put karma-read-json.js in your Karma files. Example:
files = [
...
'bower_components/karma-read-json/karma-read-json.js',
...
]
Make sure your JSON is being served by Karma. Example:
files = [
...
{pattern: 'json/**/*.json', included: false},
...
]
Use the readJSON function in your tests. Example:
var valid_respond = readJSON('json/foobar.json');
$httpBackend.whenGET(/.*/).respond(valid_respond);
I've been struggling to find a solution to loading external data into my testcases.
The above link: http://dailyjs.com/2013/05/16/angularjs-5/
Worked for me.
Some notes:
"defaultJSON" needs to be used as the key in your mock data file, this is fine, as you can just refer to defaultJSON.
mockedDashboardJSON.js:
'use strict'
angular.module('mockedDashboardJSON',[])
.value('defaultJSON',{
fakeData1:{'really':'fake2'},
fakeData2:{'history':'faked'}
});
Then in your test file:
beforeEach(module('yourApp','mockedDashboardJSON'));
var YourControlNameCtrl, scope, $httpBackend, mockedDashboardJSON;
beforeEach(function(_$httpBackend_,defaultJSON){
$httpBackend.when('GET','yourAPI/call/here').respond(defaultJSON.fakeData1);
//Your controller setup
....
});
it('should test my fake stuff',function(){
$httpBackend.flush();
//your test expectation stuff here
....
}
looks like your solution is the right one but there are 2 things i don't like about it:
it uses jasmine
it requires new learning curve
i just ran into this problem and had to resolve it quickly as i had no time left for the deadline, and i did the following
my json resource was huge, and i couldn't copy paste it into the test so i had to keep it a separate file - but i decided to keep it as javascript rather than json, and then i simply did:
var someUniqueName = ... the json ...
and i included this into karma conf includes..
i can still mock a backend http response if needed with it.
$httpBackend.whenGET('/some/path').respond(someUniqueName);
i could also write a new angular module to be included here and then change the json resource to be something like
angular.module('hugeJsonResource', []).constant('SomeUniqueName', ... the json ... );
and then simply inject SomeUniqueName into the test, which looks cleaner.
perhaps even wrap it in a service
angular.module('allTestResources',[]).service('AllTestResources', function AllTestResources( SomeUniqueName , SomeOtherUniqueName, ... ){
this.resource1 = SomeUniqueName;
this.resource2 = SomeOtherUniqueName;
})
this solutions was faster to me, just as clean, and did not require any new learning curve. so i prefer this one.
I was looking for the same thing. I'm going to try this approach. It uses the config files to include the mock data files, but the files are a little more than json, because the json needs to be passed to angular.module('MockDataModule').value and then your unit tests can also load multiple modules and then the value set is available to be injected into the beforeEach inject call.
Also found another approach that looks promising for xhr requests that aren't costly, it's a great post that describes midway testing, which if I understand right lets your controller/service actually retrieve data like in an e2e test, but your midway test has actual access to the controller scope (e2e doesn't I think).
There are Karma preprocessors that work with JSON files also. There is one here:
https://www.npmjs.org/package/karma-ng-json2js-preprocessor
And shameless plug, this is one I developed that has RequireJS support
https://www.npmjs.org/package/karma-ng-json2js-preprocessor-requirejs
You can use the karma-html2js-preprocessor to get the json files added to the __html__ global.
see this answer for details: https://stackoverflow.com/a/22103160/439021
Here is an alternative to Cameron's answer, without the need for jasmine-jquery nor any extra Karma config, to test e.g. an Angular service using $resource :
angular.module('myApp').factory('MyService', function ($resource) {
var Service = $resource('/:user/resultset');
return {
getResultSet: function (user) {
return Service.get({user: user}).$promise;
}
};
});
And the corresponding unit test :
describe('MyServiceTest', function(){
var $httpBackend, MyService, testResultSet, otherTestData ;
beforeEach(function (done) {
module('myApp');
inject(function ($injector) {
$httpBackend = $injector.get('$httpBackend');
MyService = $injector.get('MyService');
});
// Loading fixtures
$.when(
$.getJSON('base/test/mock/test_resultset.json', function (data) { testResultSet = data; }),
$.getJSON('base/test/mock/test_other_data.json', function (data) { otherTestData = data; })
).then(done);
});
it('should have some resultset', function() {
$httpBackend.expectGET('/blahblahurl/resultset').respond(testResultSet);
MyService.getResultSet('blahblahurl').then(function (resultSet) {
expect(resultSet.length).toBe(59);
});
$httpBackend.flush();
});
});
Related
So I had a situation where we had to hit a "restful" service not under our control, where in order to get json back from the service on a GET call, we have to pass Content-Type="application/json" in the header. Only problem is that Angular strips the Content-Type from request headers on a GET. I found a blog post that suggested using a decorator on $httpBackend that allows us to intercept the call before it is sent and add back the content type:
angular
.module('MyApp')
.decorator('$httpBackend', [
'$delegate', function($delegate) {
return function() {
var contentType, headers;
headers = arguments[4];
contentType = headers != null ? headers['X-Force-Content-Type'] : null;
if (contentType != null && headers['Content-Type'] == null)
headers['Content-Type'] = contentType;
return $delegate.apply(null, arguments);
};
}]);
so, that works beautifully! Now our problem is that it has broken all unit tests where we used the mock $httpBackend service. The only error we get is "undefined".
Ex. unit test method:
it('should return service.model.error if service returns an exception code from EndProject',
inject(function($httpBackend) {
var mockResponse = sinon.stub({ 'exception': 'Unable to retrieve service data' });
$httpBackend.whenPUT(this.endProjectUrl).respond(mockResponse);
var data;
this.service.EndProject().then(function(fetchedData) {
data = fetchedData;
});
$httpBackend.flush();
expect(data.error.state).toBe(true);
expect(data.error.message).toEqual('Unable to retrieve service data');
}));
PhantomJS 2.1.1 (Mac OS X 0.0.0) projectService EndProject should return service.model.error if service returns an exception code from EndProject FAILED
undefined
/Users/mlm1205/Documents/THDSource/bolt-projects/html_app/src/app/components/services/project/projectService.spec.js:213:41
invoke#/Users/mlm1205/Documents/THDSource/bolt-projects/html_app/bower_components/angular/angular.js:4560:22
workFn#/Users/mlm1205/Documents/THDSource/bolt-projects/html_app/bower_components/angular-mocks/angular-mocks.js:2518:26
The listed decorator covers simple monkey-patching scenario where patched function isn't a constructor and has no static properties and methods.
This is true for $httpBackend in ng module, it is just a factory function with no extra properies.
This is not true for ngMock and ngMockE2E modules that override $httpBackend and have static methods, at least some of them are documented.
This means that generally safe recipe (it doesn't cover non-enumerable and inherited properties) for monkey-patching a factory function is
app.decorator('$httpBackend', ['$delegate', function ($delegate) {
var $httpBackend = function () {
...
return $delegate.apply(null, arguments);
};
angular.extend($httpBackend, $delegate);
return $httpBackend;
}]);
Regardless of that, it is a good habit to modularize the app to the level where units can be tested in isolation with no excessive moving parts (this issue is an expressive example why this is important). It is convenient to have app (bootstrapped in production), app.e2e (bootstrapped in e2e tests), app.common (common denominator), app.unitA (loaded in app.common and can be loaded separately in unit test), etc.
Most of application-wide code (config and run blocks, routing) may be moved to separate modules and loaded only in modules that directly depend on them. Unless this is a spec that tests decorator unit itself, decorator module shouldn't be loaded.
Also notice that Chrome may offer superior experience than PhantomJS when debugging spec errors.
While I marked estus's answer as the solution, based purely on what my question was...in the end, ultimately it wasn't the end result we went with. In a case of not seeing the forest through the trees, the simplest solution was to add an empty data element to the $http call's config. I had tried it before and it didn't work (or so it seemed), but after playing with it again, it did in fact work and we were able to remove the decorator from the application.
return $http.get(getItemInformationUrl + params, { dataType: 'json', data: '', headers: {'Content-Type': 'application/json'} }).then(getItemInformationCompleted).catch(getItemInformationFailed);
My application needs some config values on application startup. Suggestions from the community is to store them as constant as separate module, preferably in separate .js file. This might work for me.
However my configuration values are also stored on the server, and dont want to duplicate those on client side, so i was thinking of making server call to get those.
Im newbie to angular, is it valid design practice to make server call in module's config method? If yes then should i just use $http service to get the values from the server?
var main = angular.module('myapp', ['AdalAngular']);
main.config(['$stateProvider',$httpProvider, adalAuthenticationServiceProvider', function ($stateProvider,$httpProvider,adalProvider) {
// $stateProvider configuration goes here
// ?????CAN I make server call here to get configuration values for adalProvider.init method below???
adalProvider.init(
{
instance: 'someurl',
tenant: 'tenantid',
clientId: 'clientid',
extraQueryParameter: 'someparameter',
cacheLocation: 'localStorage',
},
$httpProvider
);
}]);
main.run(["$rootScope", "$state", .....
function ($rootScope, $state,.....) {
// application start logic
}]);
main.factory("API", ["$http", "$rootScope", function ($http, $rootScope) {
// API service that makes server call to get data
}]);
EDIT1
So based on suggestions below I'm going with declaring constant approach. Basically I will have separate config.js file and during deployment process I will overwrite the config.js file with respective environment based config.js file.
Question
If have to 10 constants then i have to pass them separately to module.config(). Is it possible to declare constant value as JSON object and somehow read it in config function so I don't have pass 10 different parameters?
angular.module('myconfig', [])
.constant('CONFIGOBJECT','{Const1:somevalue,Const2:somevalue,Const3:somevalue,Const4:somevalue}');
and then how do I read the values in config method?
var main = angular.module('myapp',['myconfig']);
main.config(['CONFIGOBJECT',function(CONFIGOBJECT){
?? How do I read CONFIGOBJECT value that is a string not json object?
})
I'll describe the solution used in project that i was working on some time ago.
It's true that you cannot use services in config phase, and it's also true, that you can use providers and constants while config phase.
So we used the next solution:
firstly, we created simple object with config, like
var config = {
someConfig1: true,
someConfig2: false,
someConfigEvents: {
event1: 'eventConfig1',
event2: 'eventConfig2'
}
etc...
}
Then we also declared angular value with jQuery lib:
app.value('jQuery', jQuery);
And now we cannot use services like $http, but we can use jQuery, so we just making ajax call to config server and extending our config:
jQuery.ajax("path/to/config", { async: false, cache: false })
.done(function (data) {
var response = angular.fromJson(data)
if (response) {
angular.extend(config, response.returnData.data);
} else {
alert('error');
}
})
.fail(function () {
alertError();
})
.always(function () {
appInit();
});
You cannot inject a service into the config section.
You can inject a service into the run section.
So, you cannot use - for example $http service to retrieve data from server inside config() , but you can do in inside run(), which initializes the provider's service.
See also more complete answer here.
Hope this helps.
UPDATE:
Why string? Why don't you simply use
.constant('CONFIGOBJECT', {Const1:somevalue,Const2:somevalue,Const3:somevalue,Const4:somevalue}
for
.constant('CONFIGOBJECT', '{Const1:somevalue,Const2:somevalue,Const3:somevalue,Const4:somevalue}'
?
Only providers are available during the config phase, not services. So you can't use $http during this phase.
But you can use it during the execution phase (in a function passed to run()).
An alternative is to have some JavaScript file dynamically generated by the server, and defining the constants you want.
Another alternative is to generate such a JS file during the build, based on some file that would be read by the server-side code.
I'm part of a team that's been working on angularjs for quite a while now and our unit tests consist of files which contain both the test logic and provider mocks of each component that tested element relies on.
This is leading to more and more code duplication, can anyone recommend an angular-ey method whereby we could maintain a test infrastructure, mock a controller or service once and be able to inject that mocked service into other tests as required?
Edit The code I'm working with is proprietary but for an example consider you have 10 or 15 services which retrieve data via HTTP requests, format the data in different ways and return that data. One example might return arrays of data so in each file which relies on the service we would have initialising code like the following ( cut down and altered so please ignore any typos or other mistakes )
myDataService: {
getDataParsedLikeY: {
[{obj1},{obj2}...]
},
getDataParsedLikeX: {
[{obja},{objb}...]
}
beforeEach(angular.mock.module('myModule'));
beforeEach(angular.mock.inject(function(myDataService) {
myDataService = function( functionName ) {
return myDataService[functionName];
}
spyOn(myDataService).and.callThrough();
})
}
If you are looking for a way not to declare same mock codes in every test files, I would suggest something like below although I'm not sure this is angular-way or not.
[1] write the code to declare your mock services like below only for unit test and import after angular-mocks.js
In your-common-mocks.js(you can name the file as you like)
(function(window){
window.mymock = {};
window.mymock.prepare_mocks = function(){
module(function($provide) {
$provide.service('myDataService', function(){
this.getDataParsedLikeY = function(){
return 'your mock value';
};
});
});
};
})(window);
If you use karma, you could write like this to import the file in karma.conf.js.
files: [
...
'(somedirctory)/angular-mocks.js',
'(somedirctory)/your-common-mocks.js'
...
]
[2] use mymock.prepare_mocks function in your test files.
describe('Some Test', function() {
beforeEach(mymock.prepare_mocks);
it('getDataParsedLikeY', inject(function(myDataService){
expect('your mock value', myDataService.getDataParsedLikeY());
As a result of above, you have to write your mock code just one time and share the mock code in your every test files. I hope this could suggest you something to achieve what you want.
If your jasmine version is 2.x, you can write test code like below without writing mock service.
spyOn(YourService, 'somemethod').and.returnValue('mock value');
In jasmine 1.3 you need to adjust little bit.
spyOn(YourService, 'somemethod').andReturn('mock value');
I hope this could help you.
Recently, I have added a security feature to an existing angular app. Here is what I got afterwards:
Chrome 3X.0.2125 (Linux) ERROR
Some of your tests did a full page reload!
Chrome 3X.0.2125 (Linux): Executed 23 of 102 (skipped 2) ERROR
This is how I have set up the security feature:
angular.module('myapp', [/*..I have omitted..*/])
.run(function(MyLoginSerivce, /*.. I have omitted ..*/)){
if(!MyLoginService.isLoggedIn()){
MyLoginService.redirectForLogin();
}else{
/* other logics */
}
}
I knew I have to add the following code to each and every test spec. But it sounds silly adding it to dozens of test files.
beforeEach(module(function($provide){
$provide.value("MyLoginServce", {
isLoggedIn: function(){
return true;
},
redirectForLogin: function {}
});
}));
Is there a way to tell Karma that use a mock service and add that piece of code only once and in one place?
Thanks
Current solution
Step 1: I saved this in a separate file, e.g. ./test/mocked.login.service.js:
var mockedLoginService = {
isLoggedIn: function(){
return true;
},
redirectForLogin: function {}
});
Step 2: I include the file in karma.conf.js, by inserting 'test/mocked.login.service.js'
Step 3: I just use it in my tests like the following:
beforeEach(module(function($provide){
$provide.value("MyLoginServce", mockedLoginService
}));
You can extract the mocked service into a separate js file as an object, include that file in the karma.conf files list, then use that object as a global in your specs.
A hot discussion raised between me and my boss about Angular E2E testing. according to vojitajina pull request we need to run a server in order to run the e2e tests. So, running e2e test involves a real server, a real server involves DB. This makes test slow.Ok, now the question is how to test e2e without involving a real server ? is there a way to use httpBackend to and the e2e angular API where I can use browser(), element(), select(), for my tests ?
[see edit below] We use a shell script to periodically capture curl requests from our seeded test server. These responses are then returned via $httpBackend.whenGet(...).respond() to intercept and return that data.
So, in our index.html
if (document.location.hash === '#test') {
addScript('/test/lib/angular-mocks.js');
addScript('/test/e2e/ourTest.js');
window.fixtures = {};
addScript('/test/fixtures/tasks/tasks_p1.js');
// the tasks_p1.js is generated and has "window.fixtures.tasks_p1 = ...json..."
addScript('/test/fixtures/tasks/tasks_p2.js');
// the tasks_p2.js is generated and has "window.fixtures.tasks_p2 = ...json..."
addScript('/test/e2e/tasks/taskMocks.js\'><\/script>');
}
ourTest.js
angular.module('ourTestApp', ['ngMockE2E','taskMocks']).
run(function ($httpBackend, taskMocks) {
$httpBackend.whenGET(/views\/.*/).passThrough();
$httpBackend.whenGET(/\/fixtures.*\//).passThrough();
taskMocks.register();
$httpBackend.whenGET(/.*/).passThrough();
});
taskMocks.js
/*global angular */
angular.module('taskMocks', ['ngMockE2E']).
factory('taskMocks', ['$httpBackend', function($httpBackend) {
'use strict';
return {
register: function() {
$httpBackend.whenGET(..regex_for_url_for_page1..).respond(window.fixtures.tasks_p1);
$httpBackend.whenGET(..regex_for_url_for_page2..).respond(window.fixtures.tasks_p2);
}
};
}]);
In a few cases our data is related to the current date; e.g. "tasks for this week", so we massage the captured data in the register() method.
EDIT
We now use Rosie to create factories for our mocked objects. So, no more CURLing test server for expected json responses. index.html no longer loads these ".js" fixtures and the mocks look like:
taskMocks.js
/*global angular */
angular.module('taskMocks', ['ngMockE2E']).
factory('taskMocks', ['$httpBackend', function($httpBackend) {
'use strict';
var tasks_p1 = [ Factory.build('task'), Factory.build('task')],
tasks_p2 = [ Factory.build('task'), Factory.build('task')]
return {
register: function() {
$httpBackend.whenGET(..regex_for_url_for_page1..).respond(tasks_p1);
$httpBackend.whenGET(..regex_for_url_for_page2..).respond(tasks_p2);
}
};
}]);
To answer your question, yes there is a way to do it without involving a real server and you can use $httpBackend to mock responses in e2e tests, but it is not as straightforward as with mocking them in the unit tests. You also need to include angular-mocks.js in the index.html as well as 'ngMockE2E' in app.js.
How to mock an AJAX request?