I've been trying to write a unit test for my Progress Service, which manages the different states of a flow (jumps to next incomplete step, marks step as complete, etc.)
I'm trying to configure $stateProvider to create a set of states and then test the service against it, but I can't get the state to change. Of course I would like to test this in an isolated way, so not depending on existing states in my application.
Here's the simple unit test:
ddescribe('Progress Steps Service', function() {
var sut, $rootScope, $state;
// holds ui.router and ProgressStepsService deps
beforeEach(module('Core'));
beforeEach(inject(
function(_$rootScope_, _$state_) {
$rootScope = _$rootScope_;
$state = _$state_;
}
));
beforeEach(inject(function(ProgressStepsService) {
// var steps = [...];
// sut = ProgressStepsService;
// sut.resolve(steps);
angular.module('TestModule', []).config(function($stateProvider) {
$stateProvider
.state('root', {url: '/root', abstract: true})
.state('root.step1', {url: '/step1'})
.state('root.step2', {url: '/step2'})
.state('root.step3', {url: '/step3'});
});
$state.go('root.step1');
$rootScope.digest();
}));
it('should init from the first step', function() {
expect($state.$current.name).toBe('root.step1');
});
});
The error I'm getting is
Error: Could not resolve 'root.step1' from state ''
I take '' is the default state, and for some reason the configured states are not navigable. I'm probably missing something very basic.
How should I configure the $stateProvider to test against arbitrary states?
Ok, so here it is what I think was happening:
I was loading $state before configuring it via $stateProvider, and in addition, I was not injecting the TestModule.
I've updated the code to look like this:
ddescribe('Progress Steps Service', function() {
var sut, $state, $rootScope;
beforeEach(function() {
// var steps = [...];
// sut = ProgressStepsService;
// sut.resolve(steps);
angular.module('TestModule', []).config(function($stateProvider) {
$stateProvider
.state('root', {url: '/root', abstract: true})
.state('root.step1', {url: '/step1'})
.state('root.step2', {url: '/step2'})
.state('root.step3', {url: '/step3'});
});
module('Core', 'TestModule');
inject(function(){});
});
beforeEach(inject(
function(_$state_, _$rootScope_) {
$state = _$state_;
$rootScope = _$rootScope_;
}
));
it('should transition to the first incomplete state', function() {
$state.go('root.step1');
$rootScope.$digest();
expect($state.$current.name).toBe('root.step1');
});
});
which does the trick.
Related
I'm trying to unit test my states in an controller. What I want to do is stub out my items factory, since I have separate unit tests that cover that functionality. I'm having a hard time getting the $injector to actually inject the factory, but it seems like I'm letting the $provider know that I want to use my fake items object when it instantiates the controller. As a disclaimer I'm brand new to angular and would love some advice if my code looks bad.
Currently when I run the test I get the message:
Error: Unexpected request: GET /home.html
No more request expected
at $httpBackend (node_modules/angular-mocks/angular-mocks.js:1418:9)
at n (node_modules/angular/angular.min.js:99:53)
at node_modules/angular/angular.min.js:96:262
at node_modules/angular/angular.min.js:131:20
at m.$eval (node_modules/angular/angular.min.js:145:347)
at m.$digest (node_modules/angular/angular.min.js:142:420)
at Object.<anonymous> (spec/states/homeSpec.js:29:16)
It appears that my mocked items factory isn't being injected into the test. When I place a console.log line in the method I want to stub in the items factory I see that line being invoked.
The code I'm looking to test is as follows:
angular.module('todo', ['ui.router'])
// this is the factory i want to stub out...
.factory('items', ['$http', function($http){
var itemsFactory = {};
itemsFactory.getAll = function() {
// ...specifically this method
};
return itemsFactory;
}])
.controller('TodoCtrl', ['$scope', 'items', function($scope, items) {
// Do things
}])
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider){
$stateProvider
.state('home', {
url: '/home',
templateUrl: '/home.html',
controller: 'TodoCtrl',
resolve: {
items: ['items', function(items){
// this is the invocation that i want to use my stubbed method
return items.getAll();
}]
}
});
$urlRouterProvider.otherwise('home');
}]);
My test looks like this:
describe('home state', function() {
var $rootScope, $state, $injector, state = 'home';
var getAllStub = sinon.stub();
var items = {
getAll: getAllStub
};
beforeEach(function() {
module('todo', function($provide) {
$provide.value('items', items);
});
inject(function(_$rootScope_, _$state_, _$injector_) {
$rootScope = _$rootScope_;
$state = _$state_;
$injector = _$injector_;
});
});
it('should resolve items', function() {
getAllStub.returns('getAll');
$state.go(state);
$rootScope.$digest();
expect($state.current.name).toBe(state);
expect($injector.invoke($state.current.resolve.items)).toBe('findAll');
});
});
Thanks in advance for your help!
Allowing real router in unit tests is a bad idea because it breaks the isolation and adds more moving parts. I personally consider $stateProvider, etc. stubs a better testing strategy.
The order matters in config blocks, service providers should be mocked before they will be injected in other modules. If the original modules have config blocks that override mocked service providers, the modules should be stubbed:
beforeAll(function () {
angular.module('ui.router', []);
});
beforeEach(function () {
var $stateProviderMock = {
state: sinon.stub().returnsThis()
};
module(function($provide) {
$provide.constant('$stateProvider', $stateProviderMock);
});
module('todo');
});
You just need to make sure that $stateProvider.state is called with expected configuration objects an arguments:
it('should define home state', function () {
expect($stateProviderMock.state.callCount).to.equal(1);
let [homeStateName, homeStateObj] = $stateProviderMock.state.getCall(0).args;
expect(homeStateName).to.equal('home');
expect(homeState).to.be.an('object');
expect(homeState.resolve).to.be.an('object');
expect(homeState.resolve.items).to.be.an('array');
let resolvedItems = $injector.invoke(homeState.resolve.items);
expect(items.getAll).to.have.been.calledOnce;
expect(resolvedItems).to.equal('getAll');
...
});
I am trying to write a unit test for my ui-router with uses resolve.
Router.js
define(['module', 'require'], function(module, require) {
'use strict';
var Router = function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/shopping');
$stateProvider
.state('shopping', {
url: '/shopping',
templateUrl: 'app/shopping.html',
resolve:{
userFactory : 'UserFactory',
checkAccess:function(userFactory){
return userFactory.checkUser();
}
}
})
.state('products', {
url: '/products',
templateUrl: 'app/products.html',
resolve:{
userFactory : 'UserFactory',
checkAccess:function(userFactory){
return userFactory.checkUser();
}
}
})
.state('notAuth', {
url: '/notAuth',
templateUrl: 'app/unauthorised.html'
});
};
module.exports = ['$stateProvider', '$urlRouterProvider', Router];
});
Within userFactory.checkUser(); i essentially check the users' rights, and redirect either to the templateUrl, or perform a:
$state.go('notAuth');
My currect .spec.js:
define(['require', 'angular-mocks', 'angular-ui-router', 'app/router'], function (require) {
'use strict';
describe('myApp/myState', function() {
var $rootScope, $state, $injector, myServiceMock, $httpBackend, state = 'shopping';
var mockResponse = {
"access": true
}
var angular = require('angular');
var myRouter = require('app/router');
beforeEach(module('ui.router'));
beforeEach(function() {
module(myRouter, function($provide) {
$provide.value('userFactory', myServiceMock = {});
});
inject(function(_$rootScope_, _$state_, _$injector_, $templateCache, _$httpBackend_) {
$rootScope = _$rootScope_.$new();
$state = _$state_;
$injector = _$injector_;
$httpBackend = _$httpBackend_;
$rootScope.$digest();
})
});
it('should transition to shopping', inject(function($state,$rootScope){
$state.transitionTo('shopping');
$rootScope.$apply();
expect($state.current.name).toBe('shopping');
}));
it('should resolve data', function() {
myServiceMock.checkUser = jasmine.createSpy('checkUser').and.returnValue(mockResponse);
// earlier than jasmine 2.0, replace "and.returnValue" with "andReturn"
$state.go(state);
$rootScope.$digest();
expect($state.current.name).toBe(state);
});
});
});
With the above test, i get the following errors:
Information: myApp/myState
Information: should transition to shopping Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Information: Watchers fired in the last 5 iterations: []
Information: http://errors.angularjs.org/1.4.0/$rootScope/infdig?p0=10&p1=%5B%5D
Information: should resolve data Expected '' to be 'shopping'.
It looks like there are a few things happening here. First, you're digesting too many times. That's the "10 $digest() iterations reached" message. To fix that one, you should remove the $rootScope.$digest() from the end of your inject function.
I think the actual failing test part is because you're not resolving your checkAccess resolve that you've configured in your route. Try adding $provide.value('checkAccess', function() {return true;}); to your $provide.value setup. That should allow checkAccess to resolve in your route, which will then allow the route to complete the state transition.
I found my answer to a similar question here: Angular Jasmine UI router inject resolve value into test
I am trying to test the routes defined for the angular-ui-router of my AngularJS application. The test is using a karma / jasmine setup. I am injecting the $state service in the test in order to check if all relevant states have been defined. However, the $state service only lists a single (empty) state. Here is how the module setup and the test setup look like:
Module Definition:
angular.module('module', ['ui.router'])
.config(function($stateProvider) {
$stateProvider
.state('state01', {
url: '/state01'
})
.state('state02', {
url: '/state02'
});
});
Test Definition:
describe('module', function() {
var $state;
beforeEach(module('module'));
beforeEach(inject(function (_$state_) {
$state = _$state_;
}));
describe('module config', function() {
it('should test stuff...', function() {
console.log($state.get());
// prints: [Object{name: '', url: '^', views: null, abstract: true}]
});
});
});
Any ideas, why the $state service might be missing the router state definitions?
I am fairly new to unit testing in angular so bear with me please!
I have a $stateProvidor setup for my app and would like to test that the routing part does work correctly.
Say I have this sort of config:
angular.module("app.routing.config", []).config(function($stateProvider, $urlRouterProvider) {
$stateProvider.state("home", {
url: "/",
templateUrl: "app/modules/home/page/home_page.html",
controller: "HomePageController",
resolve: {
setPageTitle: function($rootScope) {
return $rootScope.pageTitle = "Home";
}
}
}).state("somethingelse", {
url: "/",
templateUrl: "app/modules/home/page/somethingelse.html",
controller: "SomeThingElseController",
resolve: {
setPageTitle: function($rootScope) {
return $rootScope.pageTitle = "Some Thing Else";
}
}
});
return $urlRouterProvider.otherwise('/');
});
I came across this blog post on how to set up unit testing for a ui-router config, so I Have tried to adopt the same approach, here is my test I am trying out:
'use strict';
describe('UI-Router State Change Tests', function() {
var $location, $rootScope, $scope, $state, $templateCache;
beforeEach(module('app'));
beforeEach(inject(function(_$rootScope_, _$state_, _$templateCache_, _$location_) {
$rootScope = _$rootScope_;
$state = _$state_;
$templateCache = _$templateCache_;
$location = _$location_;
}));
describe('State Change: home', function() {
beforeEach(function() {
$templateCache.put(null, 'app/modules/home/page/home_page.html');
});
it('should go to the home state', function() {
$location.url('home');
$rootScope.$digest();
expect($state.href('home')).toEqual('#/');
expect($rootScope.pageTitle).toEqual('Home');
});
});
});
When running the test I am getting this error in the output:
Error: Unexpected request: GET app/modules/home/page/home_page.html
Clearly I am doing something wrong here, so any help or pointers would be much appreciated.
I did come across $httpBackend, is this something I should also be using here, so telling my test to expect a request to the html page my state change test is making?
This is almost certainly down to a partial html view (home_page.html) being loaded asynchronously during app / test runtime.
In order to handle this, you can preprocess your html partials into Javascript strings, which can then be loaded synchronously via your tests.
Have a look at karma-ng-html2js-preprocessor which should solve your problem.
I am writing unit test which is using $stateProvider(the code is shown below for both the code and its test file).While executing this, it is giving error- "Error: Could not resolve 'app.history' from state ''".
$stateProvider
.state('app',
{
url: "/app",
templateUrl: "pages/app/index.html",
controller: function($state) {
$state.go('app.history');
}})
.state('app.history',
{ url: "/history",
templateUrl: "pages/app/modules/History/partials/history.html"
})
Unit test code -
describe("Unit tests for config.jst", function() {
var $rootScope, $injector, $state;
beforeEach(module('ui.router'));
beforeEach(inject(function(_$rootScope_, _$state_, _$injector_, $templateCache) {
$rootScope = _$rootScope_;
$injector = _$injector_;
$state = _$state_;
$templateCache.put("pages/app/index.html", "");
$templateCache.put("pages/app/modules/History/partials/history.html", "");
}));
describe("states", function() {
var state = "app.history";
it("verify state configuration", function() {
//var config = $state.get(state);
$state.go(state);
$rootScope.$digest();
//console.log($state);
expect($state.current.name).to.be.equal(state);
});
});
});
You forgot to include application module.
Add
module('your_application_module_name');
below
beforeEach(module('ui.router'));