I am writing a jasmine test for my DetailCtrl. I have 10 json file each with file names like this
1.json
2.json
3.json
in my data folder
Here is my Detail Ctrl
backpagecontrollers.controller('DetailCtrl', function($scope, $stateParams, $http) {
$http.get('data/' + $stateParams.listingId + '.json').success(function(data) {
$scope.extrainfo = data;
});
});
The detail controller is fetching each 1.json, 2.json, 3.json file from my data folder.
Here is a part of my route
.state('listingdetail', {
url: "/listings/:listingId",
templateUrl: "partials/detail.html",
controller: 'DetailCtrl'
})
Lets head back to the test, I injected both the $stateParams and the $state into the test.
I want to test that for each json file above the images exist inside my json file.
I am setting the httpbackend to get the local host url plus the listingId from the $stateparams which I configured as part of the routes but the listingId is coming back as undefined. Am I suppose to inject something else into my test?
describe('Detail Ctrl', function() {
var scope, ctrl, httpBackend, stateparams, listingId;
beforeEach(angular.mock.module("backpageApp"));
beforeEach(angular.mock.inject(function($controller, $rootScope, _$httpBackend_, $stateParams, $state) {
httpBackend = _$httpBackend_;
stateparams = $stateParams;
listingId = stateparams.listingId;
httpBackend.expectGET('http://localhost:8000/#/listings/' + listingId).respond([{id: 1 }, {id: 2}, {id:3}, {id:4}, {id:5}, {id:6}, {id:7}, {id:8}, {id:9}, {id:10}]);
scope = $rootScope.$new();
ctrl = $controller("DetailCtrl", {$scope:scope});
}));
it('the images for each listing should exist', function() {
httpBackend.flush();
expect(scope.images).toBe(true)
});
});
I am getting this error
Error: Unexpected request: GET data/undefined.json
Expected GET http://localhost:8000/#/listings/undefined
I think you might be misunderstanding how the router is working with the controller. When you're unit testing a controller, you're not executing a route or entering a ui-router state. Those states and routes are what trigger controllers to be executed when the application is running normally. But in a unit test, you're executing the controller explicitly using $controller. So you're skipping the routing part altogether. Which means you need to mock the object that the ui-router would normally create for you, $stateparams.
describe('Detail Ctrl', function() {
var scope, ctrl, httpBackend, stateparams, listingId;
beforeEach(angular.mock.module("backpageApp"));
//don't need to inject state or stateparams here
beforeEach(angular.mock.inject(function($controller, $rootScope, _$httpBackend_) {
httpBackend = _$httpBackend_;
stateparams = { listingId: 1 }; //mock your stateparams object with your id
//you should be expecting the get request url from the controller, not the route
httpBackend.expectGET('data/' + stateparams.listingId + '.json').respond([{id: 1 }, {id: 2}, {id:3}, {id:4}, {id:5}, {id:6}, {id:7}, {id:8}, {id:9}, {id:10}]);
scope = $rootScope.$new();
//pass your mock stateparams object to the controller
ctrl = $controller("DetailCtrl", {$scope:scope, $stateParams:stateparams});
}));
it('the images for each listing should exist', function() {
httpBackend.flush();
//I don't see images set in your controller, but you
//could check scope.extrainfo here
expect(scope.images).toBe(true)
});
});
Adding the stateMock.js and then including the module
beforeEach(function() {
module('stateMock');
module('mean');
module('mean.system');
module('mean.companies');
});
code here for stackMock.js: github code for stateMock
Reference: UI-router interfers with $httpbackend unit test, angular js.
Related
I want to unit test my controller. I started with basic test assertions of expect API. But I am facing challenge in mocking scope methods inside a conditional check. I am getting an undefined error since it is not available under scope, only the global logout() method is available.
I tried mocking the localStorageService using spyOn as true to satisfy the condition, but that's still of no help. Any solution will be of great help to get me kickstarted.
Controller:
angular.module('app').controller('sampleCtrl',
function($scope, $state, $http, $rootScope, localStorageService) {
if (!(localStorageService.get('isAuthenticated'))) {
$state.go('home');
}
if (localStorageService.get('isAuthenticated') === true) {
//http post calls made here to perform certain operation on page load
$scope.someMethod = function(){
//do something
}
}
$scope.logOut = function() {
localStorageService.set('property', '');
localStorageService.set('isAuthenticated', false);
$state.go('home');
};
});
Karma:
'use strict';
describe('Controller: sampleCtrl', function() {
/** to load the controller's module */
beforeEach(module('app'));
var sampleCtrl,scope,httpBackend,deferred,rootScope;
beforeEach(inject(function ($controller,_$rootScope_,$httpBackend,$q) {
var store = {};
scope= _$rootScope_.$new(); // creates a new child scope of $rootScope for each test case
rootScope = _$rootScope_;
localStorageService = _localStorageService_;
httpBackend = $httpBackend;
httpBackend.whenGET(/\.html$/).respond('');
spyOn(localStorageService, 'set').and.callFake(function (key,val) {
store[key]=val;
});
spyOn(localStorageService, 'get').and.callFake(function(key) {
return store[key];
});
sampleCtrl = $controller('sampleCtrl',{
_$rootScope_:rootScope,
$scope:scope,
$httpBackend:httpBackend,
_localStorageService_:localStorageService
// add mocks here
});
localStorageService.set('isAuthenticated',true);
}));
/**ensures $httpBackend doesn’t have any outstanding expectations or requests after each test*/
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('sampleCtrl to be defined:',function(){
httpBackend.flush();
expect(sampleCtrl).toBeDefined();
});
// failing test case - scope.someMethod not available in scope
it('is to ensure only authenticated user can access the state methods',function(){
localStorageService.get('isAuthenticated');
httpBackend.flush();
expect(scope.someMethod).toBeDefined();
});
});
I've managed to get it work.
The problem was that localStorageService did not have isAuthenticated set to true on starting the controller. Place setting it to true before calling the controller.
Like many folks, I'm new to testing Angular with Jasmine and am struggling to get this right. I use ui-router to do my routing and right now, the problem I'm having is that the $state.current.name in the test is an empty string and I have no idea why it does that.
This is the code in my routing module:
var cacRouteViewMod = angular.module('cacRouteViewMod', ['ui.router', 'cacLib']);
cacRouteViewMod.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('countries', {
url: '/countries',
templateUrl: 'countries/countries.html',
controller: 'countriesCtrl',
resolve : {
countries: ["getCountry", function(getCountry) {
return getCountry();
}]
}
});
}]);
and the test I wrote is this:
describe('cac_app_views (routing)', function() {
var $rootScope,
$state,
$injector,
getCountryMock,
state = 'countries';
beforeEach(function() {
module('cacRouteViewMod', function($provide, $urlRouterProvider) {
$urlRouterProvider.deferIntercept();
$provide.value('getCountry', getCountryMock = {});
});
inject(function(_$rootScope_, _$state_, _$injector_, $templateCache) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
$templateCache.put('countries/countries.html', '');
})
});
// Test 1
it('should respond to URL', function() {
expect($state.href(state)).toEqual('#/countries');
});
// Test 2
it('should resolve getCountry', function() {
getCountryMock = jasmine.createSpy('getCountry').and.returnValue('nanana');
$rootScope.$apply(function() {
$state.go('countries');
});
expect($state.current.name).toBe('countries');
expect($injector.invoke($state.current.resolve.countries)).toBe('nanana');
});
});
Test 1 is fine, but test 2 is the issue. The test fails because it expected '' to be 'countries'.
When I log $state.current to the console it gives
Object {name: "", url: "^", views: null, abstract: true}
I'm getting pretty desperate at this point. Could anyone help me understand/solve this problem?
I solved this in this manner:
By reading similar stockoverflow posts, I put a listener for $stateChangeError and it got triggered. I logged out the error data and saw that it's a typeError: getCountry is not a function. This caused the $state not to be updated, and therefore still contains the original(empty) $state.
I fixed the $provide.value to such:
$provide.value('getCountry', getCountryMock = function() {return 'nanana';});
which says "whenever getCountry is called, provide getCountryMock instead, which is a function that returns a string 'nanana'.
Now the tests all work the way I want them to.
Note: I found that getCountryMock = jasmine.createSpy..... line of code to be obsolete with my other change to $provide.value() so I commented it out.
According to the documentation $state.go returns a promise.
You should use done() function from Jasmine in order to test such a code: http://ng-learn.org/2014/08/Testing_Promises_with_Jasmine/
We are using ui-router 0.2.10.
I am injecting a resolve object as a parameter into my controller, which is then setting a scope variable in the controller. It works perfectly on the app like so:
state provider
$stateProvider.state('myState', {
resolve:{
foo: function(){
return 'bar';
},
url: '/',
templateUrl: 'index.html',
controller: 'FooCtrl'
})
controller
app.Controllers.controller('FooCtrl', ['$scope', '$state', 'foo',
function ($scope, $state, $log, Zone, foo) {
$scope.testVar = foo
console.log($scope.testVar);
}])
'Bar' is then logged to the console as expected in Chrome.
But when running tests using Karma, the resolve object is now undefined, which fails the test. Here is the test code:
describe('controllers', function(){
var $rootScope,
$scope,
$state
beforeEach(module('app'))
beforeEach(inject(function($injector) {
$state = $injector.get('$state')
$rootScope = $injector.get('$rootScope')
$scope = $rootScope.$new()
$controller = $injector.get('$controller')
}))
it('FooCtrl should exist', inject( function() {
$state.go('myState')
$rootScope.$apply()
$controller = $controller('FooCtrl', {
'$scope': $scope
})
$rootScope.$apply()
assert.equal($scope.testVar, "bar", "these strings are equal")
}))
})
This error is presented (the resolve object in my case is called resolvedRouteModels):
[$injector:unpr] Unknown provider: fooProvider <- foo
http://errors.angularjs.org/1.3.0-build.2921+sha.02c0ed2/$injector/unpr?p0=fooProvider%20%3C-%20foo
Any help would be much appreciated, and please let me know if you have encountered this problem.
When you instantiate your controller, Angular usually can figure out how to satisfy the controller's dependencies. In this case, it doesn't know about UI-Router's "resolve" functionality.
One way to address this is to supply this dependency yourself in the test, the same way you are passing in the scope to the controller:
var foo = 'bar'; // whatever
$controller = $controller('FooCtrl', {$scope: $scope, foo: foo} );
Note, you could also create a mock $state object and pass that into the controller the same way, if you wanted to incorporate that into your tests.
my assumption is your Angular set up is perfect, if that's the case, you might want to test your code this way. I've used Jasmine 2 syntax.
describe('Foo Controller', function() {
var $scope, $state, controller, Zone, foo, $log;
beforeEach(module('app'));
beforeEach(inject(function($controller) {
$scope = {};
$state = {};
$log = {};
Zone = {};
foo = {};
controller = $controller;
}));
it('should log the value foo', function() {
spyOn(console, 'log');
controller('FooCtrl', { $scope, $state, $log, Zone, foo });
expect($scope.testVar).toEqual({});
expect(console.log).toHaveBeenCalledWith({});
});
it('should log the value foo', function() {
spyOn(console, 'log');
// You could change the value of foo i.e.
foo = 'create more spies than fbi';
controller('FooCtrl', { $scope, $state, $log, Zone, foo });
expect($scope.testVar).toEqual('create more spies than fbi');
expect(console.log).toHaveBeenCalledWith('create more spies than fbi');
});
});
Once again I hope this helps. Peace.
I've seen similar questions asked, but not one of them resolves the root of the problem, which assumes the following:
I can't use whenGET('').passThrough(), because I'm not using ngMockE2E.
I shouldn't need to use ngMockE2E because I'm writing a unit test for a directive that literally does nothing but spit out "bar".
One suggestion was to use a proxy server to serve-up these HTTP responses, but doesn't that defeat the purpose of a unit test?
Now, let me show you my directive:
angular.module('app')
.directive('foo', function () {
return {
restrict: 'A',
templateUrl: 'templates/bar.html'
};
});
And here is the unit test:
describe('Directive: foo', function () {
// load the directive's module
beforeEach(module('app'));
var $compile;
var $rootScope;
var $httpBackend;
beforeEach(inject(function(_$compile_, _$rootScope_, $injector) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$httpBackend = $injector.get('$httpBackend');
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should be bar', inject(function($templateCache) {
$templateCache.put('templates/bar.html', 'bar');
var element = $compile('<div data-foo></div>')($rootScope);
//$httpBackend.whenGET(/^\/translations\//).passThrough(); // doesn't work
$httpBackend.expectGET(/\/translations\//).respond('201', ''); // works
$rootScope.$digest();
$httpBackend.flush();
expect(element.text()).toBe('bar'); // works
}));
});
Now, the test works just fine with all this bogus $httpBackend business in there, but this totally doesn't belong in my unit tests! How do I pull this $httpBackend crap out and stop the AngularJS app config blocks from running in my directive unit tests with a templateUrl?
Note: This only happens when there is a templateUrl in the directive.
BTW, the code that's triggering this HTTP request in the app config block is:
$translatePartialLoaderProvider.addPart('index');
$translateProvider.useLoader('$translatePartialLoader', {
urlTemplate: '/translations/{part}/{lang}.json'
});
But I don't think that's necessarily relevant.
Put your directives into their own module and include that module into your main app module. Then you can test your directive module without loading up your app module.
e.g.
angular.module('app-directives', [])
.directive('foo', function () {
return {
restrict: 'A',
templateUrl: 'templates/bar.html'
};
});
angular.module('app', ['app-directives'])
//Run your config stuff here
so I'm new to angularjs and its mocking library. I am trying to test that a specific GET request is made, but I always get this error for the 2nd assertion and can't figure out why:
Error: Unsatisfied requests: GET /1.json
Is there anything I messed up with my code below?
App.js
var App = angular.module('App', []).config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'views/main.html',
controller: 'MainCtrl'
}).when('/Items', {
templateUrl: 'views/items.html',
controller: 'Ctrl'
}).otherwise({
redirectTo: '/'
});
}]);
Ctrl.js
function Ctrl($scope, $http, $filter) {
$scope.items = [];
$http.get('/1.json').success(function(data) {$scope.items = data.items;});
}
Ctrl.$inject = ["$scope","$http", "$filter"];
Spec/Ctrl.js
describe('Controller: Ctrl', function() {
var $httpBackend;
// load the controller's module
beforeEach(module('App'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
// backend definition common for all tests
$httpBackend.whenGET('/1.json').respond('Response!');
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
var Ctrl, scope;
// Initialize the controller and a mock scope
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
Ctrl = $controller('Ctrl', {
$scope: scope
});
}));
it('should initialize with 0 items', function() {
expect(scope.items.length).toBe(0);
$httpBackend.flush();
});
it('should make store request', function(){
var controller = scope.$new(Ctrl);
$httpBackend.expectGET('/1.json');
$httpBackend.flush();
});
});
EDIT: added app and controller code.
I finally got my unit tests working! Mostly because I restructured my application to make more sense and be more modular.
I'll try to give information to help the next person that runs into this:
first of was I switched to using the $resource instead of $http.
instead of injecting $injector, I injected $httpBackend like so:
beforeEach(inject(function(_$httpBackend_, $rootScope, $route, $controller){
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('/path/to/api').respond([{id:1}]);
instead of referencing 'Ctrl' as a string, I passed in the actual class
Ctrl = $controller('Ctrl', {
$scope: scope
});
became
var ProductsCtrl = ['$scope', function($scope){ ... }];
Ctrl = $controller(ProductsCtrl, {
$scope: scope
});`
Make sure you are referencing the angular-resources.js file if you are using $resources
I'm really loving Angularjs; I think it just takes some time to wrap your head around how to test. Best of luck out there!