within a controller i have a function which uses $state.transitionTo to "redirect" to another state.
now i am stuck in testing this function, i get always the error Error: No such state 'state-two'. how can i test this? it its totally clear to me that the controller does not know anything about the other states, but how can i mock this state?
some code:
angular.module( 'mymodule.state-one', [
'ui.state'
])
.config(function config($stateProvider) {
$stateProvider.state('state-one', {
url: '/state-one',
views: {
'main': {
controller: 'MyCtrl',
templateUrl: 'mytemplate.tpl.html'
}
}
});
})
.controller('MyCtrl',
function ($scope, $state) {
$scope.testVar = false;
$scope.myFunc = function () {
$scope.testVar = true;
$state.transitionTo('state-two');
};
}
);
describe('- mymodule.state-one', function () {
var MyCtrl, scope
beforeEach(module('mymodule.state-one'));
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
MyCtrl = $controller('MyCtrl', {
$scope: scope
});
}));
describe('- myFunc function', function () {
it('- should be a function', function () {
expect(typeof scope.myFunc).toBe('function');
});
it('- should test scope.testVar to true', function () {
scope.myFunc();
expect(scope.testVar).toBe(true);
expect(scope.testVar).not.toBe(false);
});
});
});
Disclaimer: I haven't done this myself, so I totally don't know if it will work and is what your are after.
From the top of my head, two solutions come to my mind.
1.) In your tests pre configure the $stateProvider to return a mocked state for the state-two That's also what the ui-router project itself does to test state transitions.
See: https://github.com/angular-ui/ui-router/blob/04d02d087b31091868c7fd64a33e3dfc1422d485/test/stateSpec.js#L29-L42
2.) catch and parse the exception and interpret it as fulfilled test if tries to get to state-two
The second approach seems very hackish, so I would vote for the first.
However, chances are that I totally got you wrong and should probably get some rest.
Solution code:
beforeEach(module(function ($stateProvider) {
$stateProvider.state('state-two', { url: '/' });
}));
I recently asked this question as a github issue and it was answered very helpfully.
https://github.com/angular-ui/ui-router/issues/537
You should do a $rootScope.$apply() and then be able to test. Note that by default if you use templateUrl you will get an "unexpected GET request" for the view, but you can resolve this by including your templates into your test.
'use strict';
describe('Controller: CourseCtrl', function () {
// load the controller's module
beforeEach(module('myApp'));
// load controller widgets/views/partials
var views = [
'views/course.html',
'views/main.html'
];
views.forEach(function(view) {
beforeEach(module(view));
});
var CourseCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
CourseCtrl = $controller('CourseCtrl', {
$scope: scope
});
}));
it('should should transition to main.course', inject(function ($state, $rootScope) {
$state.transitionTo('main.course');
$rootScope.$apply();
expect($state.current.name).toBe('main.course');
}));
});
Also if you want to expect on that the transition was made like so
expect(state.current.name).toEqual('state-two')
then you need to scope.$apply before the expect() for it to work
Related
First, I never seen Angular and Jasmine until several months ago. So I spend two or three months studying this in the practices of a company, and finally they've sent me to try test a controller/service in Visual Studio Code.
I have this variable in the controller:
vm.option = $state.param.option;
And in the spec.js I create a it with this:
it('"option" should be defined', function () {
expect(ctrl.option).toBeDefined();
});
Previously, I inject in beforeEach a $controller, $rootScope, _$log_, $injector and the service. I need something special for test this variable? I tried inject _$state_ but the message Expected undefined to be defined appears too.
I appreciate all help, and sorry for my bad english.
Edit:
The spec.js :
'use strict';
describe('app/specs/spec.js', function () {
var scope, $log, service, ctrl, state/*, testedStateExample*/;
beforeAll(function () {} );
beforeEach(angular.mock.module('App.moduleExample'));
beforeEach(function () {
module(function ($provide){
$provide.constnt('APP_CONFIG', {});
});
});
beforeEach(angular.mock.inject(function ($controller, $state, $rootScope, _$log_, _service_){
service = _service_;
scope = $rootScope.$new();
$log = _$log_;
$state = $state;
state = { params: { option: 'E' }}
ctrl = $controller('controllerExample', {
$scope: scope,
service: service,
$log: $log
});
//testedStateExample = new ctrl(state);
});
it('"option" should be defined', function () {
expect(state.params).toBeDefined();
});
});
There is typo in your controller: $state doesn't have param property but params.
Besides, you have to also define params.option on injected $state object in your tests because in injected $state it's rather not set so your controller can't read it from $state - but how to do this depends on your code details. state.params are set according to URL and route config but when you test standalone controller there is no URL nor route config and as a result $state.params is empty.
The best way to solve your problem is to mock injected $state:
Lets say that you have following controller:
function MyController($state) {
this.option = $state.params.option
...
}
In your test spec you can mock $state service and pass this mock to your controller as argument:
var mockState = {
params: {
option: 'TEST'
}
}
var testedControllerInstance = new MyController(mockState)
}
...
expect(testedControllerInstance.option).toBe('TEST');
Answer update according to updated question:
You've forgotten to inject state into your controller:
beforeEach(angular.mock.inject(function ($controller, $state, rootScope, _$log_, _service_){
service = _service_;
scope = $rootScope.$new();
$log = _$log_;
var state = { params: { option: 'E' }}
ctrl = $controller('controllerExample', {
$scope: scope,
service: service,
$log: $log,
$state: state
});
});
...
})
;
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'm testing a directive ('planListing') that has a dependency on a service called 'planListingService'. This service has a dependency to another service called 'ajax' (don't shoot the messenger for the bad names).
I'm able to compile the directive, load its scope and get the controller WITH A CAVEAT. As of now I am being forced to mock both services 'planListingService' and 'ajax' otherwise I will get an error like this:
Error: [$injector:unpr] Unknown provider: ajaxProvider <- ajax <- planListingService
http://errors.angularjs.org/1.3.20/$injector/unpr?p0=ajaxProvider%20%3C-%20ajax%20%3C-%20planListingService
I thought that because I was mocking up the 'planListingService' that I wouldn't have to actually bother with any implementation nor any dependencies of this service. Am I expecting too much?
Here is the code in a nutshell:
planListing.js
angular.module('myApp')
.directive('planListing', planListing)
.controller('planListingCtrl', PlanListingCtrl);
function planListing() {
var varDirective = {
restrict: 'E',
controller: PlanListingCtrl,
controllerAs: 'vm',
templateUrl: "scripts/directives/planListing/planListing.html";
}
};
return varDirective;
}
PlanListingCtrl.$inject = ['planListingService'];
function PlanListingCtrl(planListingService) {
...
}
planListingService.js
angular.module('myApp')
.factory('planListingService', planListingService);
planListingService.$inject = ['$q', 'ajax'];
function planListingService($q, ajax) {
...
}
ajax.js
angular.module('myApp')
.factory('ajax', ['backend', '$browser', 'settings', '$http', '$log',
function (backend, $browser, settings, $http, $log) {
...
planListing.spec.js
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
module('myApp');
module('my.templates');
beforeEach(module(function ($provide){
// This seems to have no effect at all, why?
$provide.service('planListingService', function () {
this.getAllPricePlans=function(){};
});
// I don't get the error if I uncomment this:
// $provide.service('ajax', function ($q) {
// this.getAllPricePlans=function(){};
// });
}));
beforeEach(function() {
module('myApp');
module('my.templates');
});
beforeEach(angular.mock.inject(function (_$compile_,_$rootScope_,_$controller_){
$compile=_$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_;
el = angular.element('<plan-listing></plan-listing>');
scope = $rootScope.$new();
$compile(el)(scope);
scope.$digest();
ctrl = el.controller('planListing');
scope = el.isolateScope() || el.scope();
vm = scope.vm;
}));
describe('testing compilation / linking', function (){
it('should have found directive and compiled template', function () {
expect(el).toBeDefined();
expect(el.html()).not.toEqual('');
expect(el.html()).toContain("plan-listing-section");
});
});
it('should have a defined controller',function(){
expect(ctrl).toBeDefined();
});
it('should have a defined scope',function(){
expect(ctrl).toBeDefined();
});
});
So why is that I need to mock up the 'ajax' service even though I am mocking up 'planListingService' which is the one calling the 'ajax' service?
Thanks!
I have been there... feels like bad start But i think your directive is depend on the service and you need to inject it in order to directive can work with this, Just by calling directive it doesn't mean that it's going to inject it in your test. It will look for it and if it's not injected it will give you error
you could do so before testing your directive
beforeEach(inject(function ($injector) {
yourService = $injector.get('yourService');
})
For documentation purposes, here is the answer (thanks #estus for noticing this):
Indeed the problem was related to the incorrect initialization of my modules. Instead of this:
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
module('myApp');
module('my.templates');
...
I should've done this:
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
beforeEach(module('myApp'));
beforeEach(module('my.templates'));
...
After that things started working again as expected.
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 have been trying to find a way of testing this controller part for a few days but keep getting stuck. Now I get a ReferenceError: Can't find variable: $modal but I have it injected so im not sure why its not working. I also know that this test I am writing doesn't really test anything important so if you have any suggestions about moving forward please let me know. And thank you to anyone who has helped me on code throughout this controller
Code:
$scope.confirmDelete = function (account) {
var modalInstance = $modal.open({
templateUrl: '/app/accounts/views/_delete.html',
controller: function (global, $scope, $modalInstance, account) {
$scope.account = account;
$scope.delete = function (account) {
global.setFormSubmitInProgress(true);
accountService.deleteAccount(global.activeOrganizationId, account.entityId).then(function () {
global.setFormSubmitInProgress(false);
$modalInstance.close();
},
function (errorData) {
global.setFormSubmitInProgress(false);
});
};
$scope.cancel = function () {
global.setFormSubmitInProgress(false);
$modalInstance.dismiss('cancel');
};
},
resolve: {
account: function () {
return account;
}
}
});
Test:
describe("confirmDelete() function", function () {
var controller, scope;
// sets scope of controller before each test
beforeEach(inject(function ($rootScope, _$modal_) {
scope = $rootScope.$new();
controller = $controller('AccountsController',
{
$scope: scope,
$stateParams: mockStateParams,
$state: mockState,
// below: in order to call the $modal have it be defined and send on the mock modal?
$modal: _$modal_,
//modalInstance: mockModalInstance,
global: mockGlobal,
accountService: mockAccountSrv
});
}));
beforeEach(inject(function ($modal, $q) {
spyOn($modal, 'open').and.returnValue({
result: $q.defer().promise
});
}));
it("make sure modal promise resolves", function () {
scope.confirmDelete(mockAccountSrv.account);
expect($modal.open).toHaveBeenCalled();
});
});
You need to set modal to a variable in order to be able to use it.
i.e
describe("confirmDelete() function", function () {
var controller, scope, $modal; //Initialize it here
//....
beforeEach(inject(function ($rootScope, _$modal_, $controller) {
$modal = _$modal_; //Set it here
And you need to inject $controller as well in order to be able to use it.
Plnkr