Backbone collection fetch not populating in Jasmine + Sinon spec - backbone.js

When I run this spec output I get "Expected 0 to equal 2." 2 is the correct length of model objects in my fixture so Sinon's fakeServer is responding properly with the mocked response. I can't figure out why my Collection has zero objects after fetch then. Any help would be really appreciated!
FYI: this is coming from following along the Backbone Sinon + Jasmine tutorial here: http://tinnedfruit.com/2011/03/25/testing-backbone-apps-with-jasmine-sinon-2.html
Spec:
describe "Todos collection", ->
describe "when fetching models from the server", ->
beforeEach ->
#todo = sinon.stub(window, "Todo")
#todos = new Todos()
#fixture = #fixtures.Todos.valid
#server = sinon.fakeServer.create()
#server.respondWith "GET", "/todos", #validResponse(#fixture)
afterEach ->
#todo.restore()
#server.restore()
it "should parse todos from the response", ->
#todos.fetch()
#server.respond()
expect(#todos.length).toEqual #fixture.response.todos.length
Model:
class window.Todos extends Backbone.Collection
model: window.Todo
url: "/todos"
comparator: (todo) ->
todo.get('priority')
parse: (res) ->
res.response.todos
EDIT:
Buck Doyle below has helped me see there is no spec problem. I have some kind of issue with my Jasmine Headless Webkit config, and if the specs are run with Jasmine standalone they pass.

Theory: you need to wait for the “server” to respond to the request before checking for the result. Mocking the response isn’t enough: the fetch is still asynchronous.
Try a waits or a more complicated-but-elegant waitsFor as described at https://github.com/pivotal/jasmine/wiki/Asynchronous-specs

Related

AngularJS Unit tests broken after $httpBackend decorator

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);

How to mock AngularJS $resource with $promise.then in jasmine specs

I use $resource to set up some API calls, and while testing I have adopted the general approach of injecting $qand then doing
mockMyService.doSomethingAsync.andReturnValue($q.when(successResponse))
This has been working out pretty well, however, I have a method that looks like the following:
# MyService
MyService.doSomethingAsync(params).$promise.then ->
$scope.isLoading = false
# MyService Spec
mockMyService =
doSomethingAsync: jasmine.createSpy('doSomethingAsync')
it 'calls service #doSomethingAsync', ->
inject ($q) -> mockMyService.doSomethingAsync.and.returnValue($q.when(response))
controller.methodThatWrapsServiceCall()
scope.$apply()
expect(mockMyService.doSomethingAsync).toHaveBeenCalled()
and unfortunately the mocking strategy outlined above doesn't seem to work when a $promise.then is chained at the end. I end up with the following error:
TypeError: 'undefined' is not an object (evaluating 'MyService.doSomethingAsync(params).$promise.then')
Methods that simply end with doSomethingAsync().$promise pass the tests without issues using this mocking strategy.
(Other info: These are Jasmine tests run with Karma and PhantomJS)
Actually, figured it out!
I just changed my mock to return and.returnValue( { $promise: $q.when(response) } )

Mocking $httpBackend - how to handle "Unexpected request, No more request expected"?

I have a Jasmine test that is coded like this:
it ("should send correct message to server to get data, and correctly set up scope when receiving it", function(){
$httpBackend.when('GET', 'https://localhost:44300/api/projectconfiguration/12').respond(fakedDtoBase);
$routeParams.projectId=fakeId; // user asks for editing project
scope.$apply(function(){
var controller=controllerToTest(); // so controller gets data when it is created
});
expect(scope.projectData).toEqual(fakedDtoBase);
});
and it kind of works, but I get the error:
Error: Unexpected request: GET views/core/main/main.html
No more request expected
at $httpBackend (C:/SVN/src/ClientApp/client/bower_components/angular-mocks/angular-mocks.js:1207:9)
at sendReq (C:/SVN/src/ClientApp/client/bower_components/angular/angular.js:7800:9)
at $http.serverRequest (C:/SVN/src/ClientApp/client/bower_components/angular/angular.js:7534:16)
(more stack trace)....
I do realise that I can mock every other call. But let's say I do not care what else my test wants to load as it may call few other things.
How I can make sure that every other requests just "happen silently", maybe offering a single dummy response for everything else?
Your test fails because a request is made which you haven't specified.
Try to add:
$httpBackend.when('GET', 'views/core/main/main.html').respond(fakedMainResponse);
Of course you should also define fakedMainResponse.
Please take a look also at the documentation (section Request Expectations vs Backend Definitions) which says:
Request expectations provide a way to make assertions about requests
made by the application and to define responses for those requests.
The test will fail if the expected requests are not made or they are
made in the wrong order.
The second paramete of $httpBackend.when is actually a RegExp. So if you provide a RegExp that will match all other requests it should work.
For those who are using the httpBackend to mock http calls in EndToEnd tests or just mocking the entire http calls for the application the solution is to add the following code in the app config section (change the regexp according your template's location):
$httpBackend.whenGET(/^\/templates\//).passThrough();
Reference: https://docs.angularjs.org/api/ngMockE2E/service/$httpBackend
Tested with angularjs 1.4 to fix similar problem while integrating ui-router
I think it's also important to notice that if you have a $digest(), your expectation should follow the $digest, like so:
_$rootScope_.$digest();
$httpBackend.when('GET', 'views/core/main/main.html').respond(fakedMainResponse);
// ...
$httpBackend.flush(); // And remember to flush at the end
you only need to add setTimeout and done property to your flush to prevent it
it('should get data in callback funcion', function (done) {
$httpBackend.whenGET(/\/my-endpoint/).respond(mockDataResponse);
apiFactory.getCurrencyFormat('en', function (res, err) {
expect(res.a).to.deep.equal(generalMock.a);
expect(res.b).to.deep.equal(generalMock.b);
});
setTimeout(function () {
done();
$httpBackend.flush();
}, 200);
});

How do I test AngularJS trusted html with Jasmine?

I would like to use Jasmine to ensure an html data value has been correctly trusted by AngularJS.
Code
The code below fetches an article via an external api and uses Angular's $sce to trust the html content held in article.Body.
getArticle: ->
defer = #$q.defer()
#getContent(url).then (result) =>
article = result.data
article.Body = #$sce.trustAsHtml article.Body
defer.resolve article
defer.promise
This code works and, as I step through it, I can see the data is returned and the html property article.Body has been correctly trusted. Now I would like to write a unit-test that confirms this.
Unit-Test
Here is my attempt at the jasmine unit-test:
describe 'when getArticle is called with valid articleId', ->
it "should call getContent and return article with trusted html", ->
getContentDefer = #$q.defer()
spyOn(#contentService, 'getContent').andReturn getContentDefer.promise
article = {Id:'1', Body: '<div>test</div>'}
#contentService.getArticle(article.Id).then (response) =>
expect(response.Body instanceof TrustedValueHolderType).toBeTruthy()
getContentDefer.resolve {data:article}
#$rootScope.$digest()
You can see that I am attempting to ensure that the returned response.Body is an instance of the AngularJS type: TrustedValueHolderType. I do not know if this is a good idea but anyway, it does not work and I receive the following error:
ReferenceError: TrustedValueHolderType is not defined
I was hoping there was a neat way, perhaps a boolean flag, that I could use to determine whether the article.Body is trusted html or just a plain html string.
Update
The accepted answer below (thanks #avowkind) gave me the hint I needed. The trick is to use the $sce.getTrustedHtml() method which takes the TrustedValueHolderType and returns the original html value. Perfect.
Passing Unit-Test
ddescribe 'getArticle', ->
it "should return an article with trusted html", ->
getContentDefer = #$q.defer()
spyOn(#contentService, 'getContent').andReturn getContentDefer.promise
body = '<div>test</div>'
article = {Id:'1', Body: body}
#contentService.getArticle(article.Id, #token).then (response) =>
expect(#$sce.getTrustedHtml(response.Body)).toEqual(body)
I am able to jasmine unit test my filter by using $sce.getTrustedHtml on the output of the filter. This works fine if you know how to inject the $sce service into the test.
e.g
/**
* A filter used to wrap code in the <pre> tag
*/
myApp.filter( 'code', ['$sce', function($sce) {
return function(input) {
var html = (input != "")? '<pre>' + input + '</pre>' : input;
return $sce.trustAsHtml(html);
};
}]);
// test output
it('wraps pre around input: ', function() {
expect($sce.getTrustedHtml(codeFilter("Hello"))).toEqual("<pre>Hello</pre>");
} );
This works for my local system tests. However I tried to build an example fiddle with it
http://jsfiddle.net/avowkind/vfWr3/1/
and this returns an error:
Unknown provider: $sceProvider <- $sce
if anyone can fix the fiddle that would be great.

Loading a mock JSON file within Karma+AngularJS test

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();
});
});

Resources