multiple get requests one controller: Unexpected request, unit testing - AngularJS - unit-testing

I can find plenty of examples of single http calls from a controller and how to test them,but no examples of multiple testing.
My first test works fine without Product.find(10) in the controller. When I add that line however the first test collapses.
The errors:
Error: Unexpected request: GET 0.0.0.0:3000/api/products
No more request expected
and
Error: Unexpected request: GET 0.0.0.0:3000/api/products
No more request expected
I've tried a number of things: including both in the before each, this gave me an undefined error, i tried using expect instead of when, I tried adding both whens to both tests, and a combination of the above. I'm clearly doing something very wrong but being an angular newbie, it's hard to work out exactly what that might be, especially with the lack of examples.. I am just looking to get my first test to pass with Product.find(10)
Here are my tests:
'use strict';
describe('productsController', function() {
var scope, $httpBackend;
var api_root = '0.0.0.0:3000/api/';
beforeEach(angular.mock.module('sprangularApp'));
beforeEach(angular.mock.inject( function($rootScope, $controller, _$httpBackend_) {
$httpBackend = _$httpBackend_;
//Get mock jsons
jasmine.getJSONFixtures().fixturesPath='base/js/tests/api_mock';
scope = $rootScope.$new();
$controller('productsController', {$scope: scope});
}));
//Start Tests
it('Should be array of all products', function() {
$httpBackend.when('GET', api_root + 'products').respond(
getJSONFixture('products.json')
);
$httpBackend.flush();
expect(scope.products[3].name).toBe('Ruby on Rails Bag');
});
it('Should instantiate a new product object from json data', function() {
$httpBackend.when('GET', api_root + 'products/10').respond(
getJSONFixture('10.json')
);
$httpBackend.flush();
expect(scope.currentProduct.name).toBe('Spree Ringer T-Shirt');
});
});
my controller that I am testing:
// Generated by CoffeeScript 1.6.3
(function() {
var sprangularControllers;
sprangularControllers = angular.module('sprangularControllers', ['sprangularServices']);
sprangularControllers.controller('productsController', [
'$scope', 'Product', function($scope, Product) {
Product.products_with_meta().$promise.then(function(response) {
return $scope.products = response.products;
});
return Product.find(10);
}
]);
}).call(this);
And the factory with the resource requests:
sprangularServices = angular.module('sprangularServices', ['ngResource'])
sprangularServices.factory('Defaults', ->
api_url: "0.0.0.0:3000/api/"
)
sprangularServices.factory('Product', ($resource, Defaults) ->
# $resource(Defaults.api_url + 'products.json')
class Product
constructor: ->
#service = $resource(Defaults.api_url + 'products/:id', {id: '#id'})
this.products_with_meta = ->
service = $resource(Defaults.api_url + 'products')
service.get()
this.find = (id) ->
service = $resource(Defaults.api_url + 'products/:id', {id: id})
service.get()
)
As per michael's suggestion I have edited my test to this, however I am still getting the exact same result:
'use strict';
describe('productsController', function() {
var $rootScope, $httpBackend, createController;
var api_root = '0.0.0.0:3000/api/';
beforeEach(angular.mock.module('sprangularApp'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
//Get mock jsons
jasmine.getJSONFixtures().fixturesPath='base/js/tests/api_mock';
$rootScope = $injector.get('$rootScope');
var $controller = $injector.get('$controller');
createController = function() {
return $controller('productsController', {'$scope' : $rootScope });
};
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
//Start Tests
it('Should be array of all products', function() {
$httpBackend.when('GET', api_root + 'products').respond(
getJSONFixture('products.json')
);
var controller = createController();
$httpBackend.flush();
expect($rootScope.products[3].name).toBe('Ruby on Rails Bag');
});
it('Should instantiate a new product object from json data', function() {
$httpBackend.when('GET', api_root + 'products/10').respond(
getJSONFixture('10.json')
);
var controller = createController();
$httpBackend.flush();
expect($rootScope.currentProduct.name).toBe('Spree Ringer T-Shirt');
});
});

Structuring my test in this way seemed to solve the issue:
'use strict';
describe('productsController', function() {
var $rootScope, $httpBackend, createController;
var api_root = '0.0.0.0:3000/api/';
beforeEach(angular.mock.module('sprangularApp'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
//Get mock jsons
jasmine.getJSONFixtures().fixturesPath='base/js/tests/api_mock';
$rootScope = $injector.get('$rootScope');
var $controller = $injector.get('$controller');
createController = function() {
return $controller('productsController', {'$scope' : $rootScope });
};
$httpBackend.when('GET', api_root + 'products').respond(
getJSONFixture('products.json')
);
$httpBackend.when('GET', api_root + 'products/10').respond(
getJSONFixture('10.json')
);
var controller = createController();
$httpBackend.flush();
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
$httpBackend.resetExpectations();
});
//Start Tests
it('Should be array of all products', function() {
expect($rootScope.products[3].name).toBe('Ruby on Rails Bag');
});
it('Should instantiate a new product object from json data', function() {
expect($rootScope.currentProduct.name).toBe('Spree Ringer T-Shirt');
});
});

I suppose the order of define the response, do the http call, flush and do the test is not right.
define how the http call should respond
$httpBackend.when('GET', api_root + 'products').respond(
getJSONFixture('products.json')
);
do the call from your code
$controller('productsController', {$scope: scope});
flush the httpBackend (e.g. simulate the asynchronous behavior of $http)
$httpBackend.flush();
do the test
expect(scope.products[3].name).toBe('Ruby on Rails Bag');
because your controller did a backend call in his constructor and is instantiated before you define what the response should be, you got the error.
Further information and an exmaple the is very close to your use case: http://docs.angularjs.org/api/ngMock.$httpBackend

Related

angularJS testing factories, returning undefined after get() - async issue

I am new to both angular and TDD and I'm looking for some help with one of my tests. The application I am making talks to an API backend, I have mocked this backend with some js fixtures.
json data:
{
"count": 25,
"total_count": 32,
"current_page": 1,
"per_page": 25,
"pages": 2,
"products": [
{"name": "test"},
{},
{},
{},
]}
Each of the products have a number of data I am only outlining the structure above.
My Passing test (jasmine)
'use strict';
describe('productsController', function() {
var scope, $httpBackend;
beforeEach(angular.mock.module('sprangularApp'));
beforeEach(angular.mock.inject( function($rootScope, $controller, _$httpBackend_) {
//Actual API 0.0.0.0:3000/api/, below is mock data only.
var api_root = '0.0.0.0:3000/api/'
$httpBackend = _$httpBackend_;
//Get mock jsons
jasmine.getJSONFixtures().fixturesPath='base/js/tests/api_mock';
$httpBackend.when('GET', api_root + 'products').respond(
getJSONFixture('products.json')
);
scope = $rootScope.$new();
$controller('productsController', {$scope: scope});
}));
//Start Tests
it('Should fetch all products', function() {
// $httpBackend.flush();
// expect(scope.products[5].name).toBe('Ruby on Rails Baseball Jersey');
$httpBackend.flush();
expect(scope.products.count).toBe(25);
expect(scope.products.products[0].id).toBe(1);
expect(scope.products.products[0].name).toBe('Ruby on Rails Tote');
expect(scope.products.products[9].id).toBe(5);
expect(scope.products.products[9].permalink).toBe('ruby-on-rails-ringer-t-shirt');
});
});
controller:
// Generated by CoffeeScript 1.6.3
(function() {
var sprangularServices;
sprangularServices = angular.module('sprangularServices', ['ngResource']);
sprangularServices.factory('Defaults', function() {
return {
api_url: "0.0.0.0:3000/api/"
};
});
sprangularServices.factory('Product', function($resource, Defaults) {
var Product;
return Product = (function() {
function Product() {
this.service = $resource(Defaults.api_url + 'products/:id', {
id: '#id'
});
}
Product.prototype.create = function(attrs) {
new this.service({
product: attrs
}).$save(function(product) {
return attrs.id = product.id;
});
return attrs;
};
Product.all = function() {
var data, service;
service = $resource(Defaults.api_url + 'products');
data = service.get();
};
return Product;
})();
});
}).call(this);
Tests:
'use strict';
describe('productsController', function() {
var scope, $httpBackend;
beforeEach(angular.mock.module('sprangularApp'));
beforeEach(angular.mock.inject( function($rootScope, $controller, _$httpBackend_) {
//Actual API 0.0.0.0:3000/api/, below is mock data only.
var api_root = '0.0.0.0:3000/api/'
$httpBackend = _$httpBackend_;
//Get mock jsons
jasmine.getJSONFixtures().fixturesPath='base/js/tests/api_mock';
$httpBackend.when('GET', api_root + 'products').respond(
getJSONFixture('products.json')
);
scope = $rootScope.$new();
$controller('productsController', {$scope: scope});
}));
//Start Tests
it('Should fetch all products', function() {
// $httpBackend.flush();
// expect(scope.products[5].name).toBe('Ruby on Rails Baseball Jersey');
$httpBackend.flush();
expect(scope.products.count).toBe(25);
expect(scope.products.products[0].id).toBe(1);
expect(scope.products.products[0].name).toBe('Ruby on Rails Tote');
expect(scope.products.products[9].id).toBe(5);
expect(scope.products.products[9].permalink).toBe('ruby-on-rails-ringer-t-shirt');
});
});
The above code works fine and the test passes with flying colors. What I would like to do however is have Product.all return the products array within the JS without the meta information like product count etc.
I modified the service so that it would return:
Product.all = function() {
var data, service;
service = $resource(Defaults.api_url + 'products');
data = service.get();
return data.products
};
It seems data.products is undefined, as is data, I have a feeling this could have something to do with flush, but I am not sure, why doesn't data.products return the array that is contained within the JSON? is it to do with get()'s synchronosity.
I think it is a synchronous issue.
Does this do what you want?
Product.all = function() {
var data, service;
service = $resource(Defaults.api_url + 'products');
return service.get().$promise.then(function (result) {
return result.products;
});
};
EDIT
If all you're doing is putting the result of Product.all on the $scope then you can (untested) do this:
Product.all = function() {
var data, service;
var products = [];
service = $resource(Defaults.api_url + 'products');
service.get().$promise.then(function (result) {
for (var i in result.products) {
products.push(result.products[i]);
}
});
return products;
};
If you wish to do something else I'd do (untested):
$scope.$watch(Product.all, function (value) {
$scope.products = value;
// other stuff here to products
}/* , true */); // You probably need to deeply watch the array for changes - I can't test it at the moment.

Testing $http calls in Angular 1.1.5

I'm running into trouble trying to mock my $http calls in Angular. When calling $httpBackend.flush() I'm getting the error: Error: No pending request to flush !
I've read up on many other posts of similar issues on here, but I still can't seem to get it to work by adding $rootScope.$digest() or adding $httpBackend.expect
I'm testing a service function:
getAll: function (success, error) {
$http.get(apiRootService.getAPIRootHTTP() + "/items")
.success(success).error(error);
},
Using this .spec:
describe ('itemAPI Module', function (){
var $httpBackend, $rootScope, itemAPI, apiRootService;
var projectID = 123;
beforeEach(module('itemAPI'));
beforeEach(module('mApp'));
/**
* Set up variables pre-insertion
*/
beforeEach( inject( function(_$rootScope_, _$httpBackend_, _apiRootService_){
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
apiRootService = _apiRootService_;
}));
describe('itemAPI factory', function (){
it('can get an instance of the metaAPI factory', inject(function(_itemAPI_){
itemAPI = _itemAPI_;
expect(metaAPI).toBeDefined();
}));
describe("get all items", function (){
it("will return an error", function (){
var url = apiRootService.getAPIRootHTTP() + "/items";
$httpBackend.whenGET(url)
.respond(400);
var error;
itemAPI.getAll(
function(data, status){
//this is success
},
function(data, status){
error = true;
});
$httpBackend.expectGET(url);
$rootScope.$digest();
$httpBackend.flush();
expect(error).toBeTruthy();
});
});
});
});

Faking a Angular Factory in a directive in jasmine

Question: How do I fake my pointFactory so I can Jasmine Unit Test it.
I have the Following Directive.
It takes the html sends it to a factory and the uses the response for some logic
CommonDirectives.directive('TextEnrichment',['PointFactory','appSettings', function (pointFactory,settings) {
return {
restrict: 'A',
link : function (scope, element, attrs) {
var text = element.html();
pointFactory.getPoints(text).then(function(response){
})}}}]);
So far my unit tests looks like this, however it doesn't work since I'm not injecting the factory.
beforeEach(module('app.common.directives'));
beforeEach(function () {
fakeFactory = {
getPoints: function () {
deferred = q.defer();
deferred.resolve({data:
[{"Text":"Some text"}]
});
return deferred.promise;
}
};
getPointsSpy = spyOn(fakeFactory, 'getPoints')
getPointsSpy.andCallThrough();
});
beforeEach(inject(function(_$compile_, _$rootScope_,_$controller_){
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('Factory to have been Called', function () {
var element = $compile('<div data-text-enrichment=""> Text </div>')($rootScope)
expect(getPointsSpy.callCount).toBe('1');
});
Update
Following advice from Felipe Skinner I have updated the test with the following
beforeEach(function(){
module(function($provide){
$provide.factory('PointFactory',getPointsSpy)
})
});
However I get the following error:
TypeError: 'undefined' is not a function (evaluating
'pointFactory.getPoints(text)')
You can use the $provide to inject your controller dependencies.
Here's my beforeEach for example:
describe('MyCtrl', function() {
var $controller,
$scope,
$httpBackend,
windowMock,
registerHtmlServiceMock,
mixPanelServiceMock,
toastMock;
beforeEach(function() {
windowMock = { navigator: {} };
registerHtmlServiceMock = {};
mixPanelServiceMock = jasmine.createSpyObj('mixpanel', ['track']);
toastMock = jasmine.createSpyObj('toast', ['error']);
module('myModule');
module(function($provide) {
$provide.value('$window', windowMock);
$provide.value('RegisterHtmlService', registerHtmlServiceMock);
$provide.value('MixPanelService', mixPanelServiceMock);
$provide.value('ToastService', toastMock);
});
inject(function(_$controller_, _$rootScope_, _$httpBackend_) {
$scope = _$rootScope_.$new();
$controller = _$controller_('CourseSelectionCtrl', { $scope: $scope });
$httpBackend = _$httpBackend_;
});
});
// my test cases
});
I haven't tried mocking a function that returns some value. Those two mocks (mixpanel-track and toast-error) are for "void" functions.
UPDATE:
Try changing the previous $provide with this type of injection then.
Change from this:
module(function($provide) {
$provide.value('$window', windowMock);
$provide.value('RegisterHtmlService', registerHtmlServiceMock);
$provide.value('MixPanelService', mixPanelServiceMock);
});
inject(function(_$controller_, _$rootScope_, _$httpBackend_) {
$scope = _$rootScope_.$new();
$controller = _$controller_('CourseSelectionCtrl', { $scope: $scope });
$httpBackend = _$httpBackend_;
});
To this:
beforeEach(inject(function(_$controller_, _$rootScope_, _$httpBackend_) {
mixPanelService = mixPanelServiceMock;
$scope = _$rootScope_.$new();
$controller = _$controller_('MyCtrl', { $scope: $scope, MixPanelService: mixPanelService });
$httpBackend = _$httpBackend_;
}));
The rest of the code should be the same, except for that. Let me know if this works

Unit testing AngularJS routes throwing Error: Unexpected request

Below is my Jasmine RoutesSpec.js
describe("Todo Routes", function(){
var route;
var rootScope;
var location;
beforeEach(function(){
module('todoApp');
inject(function($route, $location, $rootScope){
route = $route;
location = $location;
rootScope = $rootScope;
});
});
it("should navigate to todo list", function(){
expect(route.current).toBeUndefined();
location.path('/todos');
rootScope.$digest();
expect(route.current.templateUrl).toBe('app/html/listTodos.html');
});
});
Below is my app.js
var todoModule = angular.module("todoApp", []);
todoModule.config(function($routeProvider){
$routeProvider.when('/todos', {
templateUrl: '../html/listTodos.html',
controller: 'TodoListController'
})
.otherwise({redirectTo: '/todos'});
});
todoModule.controller("TodoListController", function($scope, $log){
$scope.todos = [{title: "My first task", done: false}];
$log.log('In list controller');
});
Executing this spec throws me the below error:
Error: Unexpected request: GET ../html/listTodos.html
No more request expected
at Error ()
at $httpBackend (C:/Learn/Javascript/todo_app/libs/angular-mocks.js:934:9)
at sendReq (C:/Learn/Javascript/todo_app/libs/angular.js:9146:9)
at $http (C:/Learn/Javascript/todo_app/libs/angular.js:8937:17)
at Function.$http.(anonymous function) (C:/Learn/Javascript/todo_app/libs/angular.js:9080:18)
at $q.when.then.then.next.locals (C:/Learn/Javascript/todo_app/libs/angular.js:7440:34)
at wrappedCallback (C:/Learn/Javascript/todo_app/libs/angular.js:6846:59)
at wrappedCallback (C:/Learn/Javascript/todo_app/libs/angular.js:6846:59)
at C:/Learn/Javascript/todo_app/libs/angular.js:6883:26
at Object.Scope.$eval (C:/Learn/Javascript/todo_app/libs/angular.js:8057:28)
This means that there is an AJAX call that goes to fetch the template.
$httpBackend.expectGET('app/html/listTodos.html').respond(200) can be put before calling path():
describe("Todo Routes", function(){
var route;
var rootScope;
var location;
var httpBackend;
beforeEach(function(){
module('todoApp');
inject(function($route, $location, $rootScope, $httpBackend){
route = $route;
location = $location;
rootScope = $rootScope;
httpBackend = $httpBackend;
});
});
it("should navigate to todo list", function(){
httpBackend.expectGET('app/html/listTodos.html').respond(200);//mimicking the AJAX call
expect(route.current).toBeUndefined();
location.path('/todos');
rootScope.$digest();
expect(route.current.templateUrl).toBe('app/html/listTodos.html');
});
});

Unit testing AngularJS controller with $httpBackend

For the life of me I can't get $httpBackend to work on a controller that does an $http get request. I've tried for hours now =)
I've reduced this to the simplest form I can below. The test passes if I
comment out the $http.get() request in the controller
comment out the "httpMock.flush()" in the test
and change "pig" and "dog" to match
That is, it's a valid, working test and app.
If I put it back in, I get the error shown at the bottom.
app/js/app.js
// Declare a module which depends on filters and services.
var myApp = angular
.module('myApp', ['ngRoute', 'myApp.filters', 'myApp.services',
'myApp.directives'])
.config(['$routeProvider' , function($routeProvider) {
$routeProvider
.when("/dashboard", {
templateUrl: "partials/dashboard.html",
controller: cDashboard
})
.otherwise({redirectTo: "/dashboard"});
}]);
// Pre-define our main namespace modules.
angular.module('myApp.directives' , []);
angular.module('myApp.filters' , []);
angular.module('myApp.services' , []);
angular.module('myApp.controllers', []);
app/js/controller.js
function cDashboard ($scope, $http) {
$scope.data = "dog";
// Fetch the actual data.
$http.get("/data")
.success(function (data) { $scope.data = data })
.error(function () {});
}
cDashboard.$inject = [ '$scope', '$http' ];
test/unit/controllerSpec.js
describe('cDashboard', function(){
var scope, ctrl, httpMock;
beforeEach(inject(function ($rootScope, $controller, $http, $httpBackend) {
scope = $rootScope.$new();
ctrl = $controller('cDashboard', {$scope: scope});
httpMock = $httpBackend;
httpMock.when("GET", "/data").respond("pig");
}));
it("should get 'pig' from '/data'", function () {
httpMock.expectGET("/data").respond("pig");
expect(scope.data).toBe("pig");
});
});
And this is the error I get in the shell:
INFO [watcher]: Changed file "/home/myApp/test/unit/controller/cDashboard.js".
Chrome 26.0 (Linux) cDashboard should get 'pig' from '/data' FAILED
Error: No pending request to flush !
at Error (<anonymous>)
at Function.$httpBackend.flush (/home/myApp/test/lib/angular/angular-mocks.js:1171:34)
at null.<anonymous> (/home/myApp/test/unit/controller/cDashboard.js:15:18)
Chrome 26.0 (Linux): Executed 1 of 1 (1 FAILED) (0.326 secs / 0.008 secs)
There are a couple problems in your test code:
The controller is created before the httpMock is configured to respond with pig. The expectGet call should happen before instantiating the controller.
The httpMock needs to flush the request
The httMock.when is unnecessary so long as you have the expectGet
Working example: http://plnkr.co/edit/lUkDMrsy1KJNai3ndtng?p=preview
describe('cDashboard', function(){
var scope, controllerService, httpMock;
beforeEach(inject(function ($rootScope, $controller, $httpBackend) {
scope = $rootScope.$new();
controllerService = $controller;
httpMock = $httpBackend;
}));
it("should get 'pig' from '/data'", function () {
httpMock.expectGET("/data").respond("pig");
ctrl = controllerService('cDashboard', {$scope: scope});
httpMock.flush();
expect(scope.data).toBe("pig");
});
});