I am migrating to the latest stable release of ui-router and am making use of the $transitions life cycle hooks to perform certain logic when certain state names are being transitioned to.
So in some of my controllers I have this kinda thing now:
this.$transitions.onStart({ }, (transition) => {
if (transition.to().name !== 'some-state-name') {
//do stuff here...
}
});
In my unit tests for the controller, previously I would broadcast a state change event on the $rootScope with the certain state names as the event args to hit the conditions I needed to test.
e.g.
$rootScope.$broadcast('$stateChangeStart', {name: 'other-state'}, {}, {}, {});
Since these state events are deprecated, whats the correct way to now trigger the $transitions.onStart(...) hooks in the tests?
I have tried just calling $state.go('some-state-name') in my tests but I can never hit my own logic within the transition hook callback function. According to the docs here, calling state.go programatically should trigger a transition, unless I am misreading?
Has anyone else managed to get unit tests for transition hooks in their controllers working for the new ui-router 1.0.x?
Full example of my controller code using a transition hook:
this.$transitions.onSuccess({ }, (transition) => {
this.setOpenItemsForState(transition.to().name);
});
test spec:
describe('stateChangeWatcher', function() {
beforeEach(function() {
spyOn(vm, 'setOpenItemsForState').and.callThrough();
});
it('should call the setOpenItemsForState method and pass it the state object', function() {
$state.go('home');
$rootScope.$apply();
expect(vm.setOpenItemsForState).toHaveBeenCalledWith('home');
});
});
My spy is never getting hit, when running the application locally this hook does get invoked as expected, so it must be something I have got setup incorrectly in my tests. Is there something extra I need to make the transition succeed in the test, since I am hooking into the onSuccess event?
Thanks
UPDATE
I raised this in the ui-router room on gitter and one of the repo contributors came back to me suggesting I check the call to $state.go('home') in my tests actually ran by adding expect($state.current.name).toBe('home'); in my test spec.
This does pass for me in my test, but I am still unable to hit the call to my function in the transition hook callback:
I'm unsure how to proceed on this, other than installing the polyfill for the legacy $stateChange events so I can use my previous code, but I'd rather not do this and figure out the proper way to test $transition hooks.
UPDATE 2
Following estus' answer, I have now stubbed out the $transitions service and also refactored my transition hook handler into a private named function in my controller:
export class NavBarController {
public static $inject = [
'$mdSidenav',
'$scope',
'$mdMedia',
'$mdComponentRegistry',
'navigationService',
'$transitions',
'$state'
];
public menuSection: Array<InterACT.Interfaces.IMenuItem>;
private openSection: InterACT.Interfaces.IMenuItem;
private openPage: InterACT.Interfaces.IMenuItem;
constructor(
private $mdSidenav,
private $scope,
private $mdMedia,
private $mdComponentRegistry,
private navigationService: NavigationService,
private $transitions: any,
private $state
) {
this.activate();
}
private activate() {
this.menuSection = this.navigationService.getNavMenu();
if (this.isScreenMedium()) {
this.$mdComponentRegistry.when('left').then(() => {
this.$mdSidenav('left').open();
});
}
this.setOpenItemsForState(this.$state.$current.name);
this.$transitions.onSuccess({ }, this.onTransitionsSuccess);
}
private onTransitionsSuccess = (transition) => {
this.setOpenItemsForState(transition.to().name);
}
private setOpenItemsForState(stateName: string) {
//stuff here...
}
}
Now in my test spec I have:
describe('Whenever a state transition succeeds', function() {
beforeEach(function() {
spyOn(vm, 'setOpenItemsForState').and.callThrough();
$state.go('home');
});
it('should call the setOpenItemsForState method passing in the name of the state that has just been transitioned to', function() {
expect($transitions.onSuccess).toHaveBeenCalledTimes(1);
expect($transitions.onSuccess.calls.mostRecent().args[0]).toEqual({});
expect($transitions.onSuccess.calls.mostRecent().args[1]).toBe(vm.onTransitionsSuccess);
});
});
These expectations pass, but Im still not able to hit my inner logic in my named hook callback onTransitionsSuccess function that make a call to setOpenItemsForState
What am I doing wrong here?
UPDATE 3
Thanks again to estu, I was forgetting I can just call my named transition hook function is a separate test:
describe('and the function bound to the transition hook callback is invoked', function(){
beforeEach(function(){
spyOn(vm, 'setOpenItemsForState');
vm.onTransitionsSuccess({
to: function(){
return {name: 'another-state'};
}
});
});
it('should call setOpenItemsForState', function(){
expect(vm.setOpenItemsForState).toHaveBeenCalledWith('another-state');
});
});
And now I get 100% coverage :)
Hopefully this will serve as a good reference to others who may be struggling to figure out how to test their own transition hooks.
A good unit test strategy for AngularJS routing is to stub a router entirely. Real router prevents units from being efficiently tested and provides unnecessary moving parts and unexpected behaviour. Since ngMock behaves differently than real application, such tests zcan't be considered proper integration tests either.
All router services in use should be stubbed. $stateProvider stub should reflect its basic behaviour, i.e. it should return itself on state call and should return $state stub on $get call:
let mockedStateProvider;
let mockedState;
let mockedTransitions;
beforeEach(module('app'));
beforeEach(module(($provide) => {
mockedState = jasmine.createSpyObj('$state', ['go']);
mockedStateProvider = jasmine.createSpyObj('$stateProvider', ['state', '$get']);
mockedStateProvider.state.and.returnValue(mockedStateProvider);
mockedStateProvider.$get.and.returnValue(mockedState);
$provide.provider('$state', function () { return mockedStateProvider });
}));
beforeEach(module(($provide) => {
mockedTransitions = jasmine.createSpyObj('$transitions', ['onStart', 'onSuccess']);
$provide.value('$transitions', mockedTransitions);
}));
A test-friendly way is to provide bound methods as callbacks instead of anonymous functions:
this.onTransitionStart = (transition) => { ... };
this.$transitions.onStart({ }, this.onTransitionStart);
Then stubbed methods can be just tested that they were called with proper arguments:
expect($transitions.onStart).toHaveBeenCalledTimes(1);
$transitions.onStart.mostRecent().args[0].toEqual({});
$transitions.onStart.mostRecent().args[1].toBe(this.onTransitionStart);
A callback function can be tested directly by calling it with expected arguments. This provides full coverage yet leaves some place for human error, so unit tests should be backed up with integration/e2e tests with real router.
Related
I'm trying to migrate to ui-router 1.0.5 and have done most of the work but there are no examples of how to test new transition hooks that replaced $stateChangeXXX events listeners.
Code before:
scope.$on('$stateChangeSuccess', this.hideSpinner_.bind(this));
After:
this.transitions_ is a $transitions service exposed by ui-router
this.transitions_.onSuccess({}, this.hideSpinner_.bind(this));
Before I was able to test it by using scope.$broadcast($stateChangeSuccess) and then scope.$apply(). This worked with ui-router 0.x:
expect(ctrl.loading).toBe(true);
expect(ctrl.showLoadingSpinner).toBe(true);
// when
scope.$broadcast('$stateChangeSuccess');
scope.$apply();
// then
expect(ctrl.loading).toBe(false);
expect(ctrl.showLoadingSpinner).toBe(false);
Any idea how to rewrite tests to work with new version of ui-router?
Well,
I faced exactly the same problem migrating from ui-router 0.3.x to 1.0.5
App
Before :
scope.$on('$stateChangeSuccess', someFunction);
After :
$transitions.onSuccess( {}, someFunction);
Tests
Before :
scope.$broadcast('$stateChangeSuccess');
scope.$apply();
After :
I call directly the callback function with a mocked transition because I just want to test what the callback does (and here it only needs trans.$to().name and trans.$from().name) :
var mockTransition = {
$to: function() { return {name: 'foo'}; },
$from: function() { return {name: 'bar'}; }
};
service.someFunction(mockTransition);
$scope.$digest();
And in some places in my tests, I want to simulate the whole transition process so I do a real one, that way the events are properly called :
it('should handle transitions error properly when trying to make transition to an abstract state', function (done) {
spyOn(console, 'error');
spyOn(transitions, 'onError');
transitions.onError({}, function(transition) {});
$stateTest.transitionTo("c3.app.offre").then(function() {
}, function() {
expect(console.error).toHaveBeenCalled();
expect(transitions.onError).toHaveBeenCalled();
done();
});
scope.$apply();
});
I've got the same question. And I chose a stupid solution that is broadcasting the 'stateChangeStart' event in a onStart hook:
$transitions.onStart({}, function($transition$){
$scope.$broadcast('$myStateChangeStart', $transition$.to(), $transition$.params('to'), $transition$.from(), $transition$.params('from'), $transition$.options(), $transition$)
})
then I do not need to change so much of the legacy code, just handling some 'concurrency' logic.
Hope it makes sense. If you've got a good practice, pls tell me.
Our Angular project moved to ES6 and Reduxjs, and now I am struggling to get controller unit tests working. Specifically, I cant seem to mock correctly when it comes to the class constructor. From what i have researched, i cant spyOn an ES6 class constructor, so i need to mock its dependencies and also accommodate the binding to lexical 'this' that ngRedux.connect() facilitates.
My test makes it to the connect function in the constructor, and then gives me the error: "'connect' is not a function"
I think i may have several things wrong here. If i comment out the connect line in the constructor, it'll get to my runOnLoad function and the error will tell me that fromMyActions isnt a function. this is because the redux connect function binds the actions to 'this', so given these issues, I take it I cant mock redux unless i provide its implementation. any advice? I am relatively new to angular as well - and my weakest area is unit testing and DI.
Here is my module and controller:
export const myControllerModule = angular.module('my-controller-module',[]);
export class MyController {
constructor($ngRedux, $scope) {
'ngInject';
this.ngRedux = $ngRedux;
const unsubscribe = this.ngRedux.connect(this.mapState.bind(this), myActions)(this);
$scope.$on('$destroy', unsubscribe);
this.runOnLoad();
}
mapState(state) {
return {};
}
runOnLoad() {
this.fromMyActions(this.prop);
}
}
myControllerModule
.controller(controllerId, MyController
.directive('myDirective', () => {
return {
restrict: 'E',
controllerAs: 'vm',
controller: controllerId,
templateUrl: htmltemplate
bindToController: true,
scope: {
data: '=',
person: '='
}
};
});
export default myControllerModule.name;
and my test:
import {myControllerModule,MyController} from './myController';
import 'angular-mocks/angular-mocks';
describe('test', () => {
let controller, scope;
beforeEach(function() {
let reduxFuncs = {
connect: function(){}
}
angular.mock.module('my-controller-module', function ($provide) {
$provide.constant('$ngRedux',reduxFuncs);
});
angular.mock.inject(function (_$ngRedux_, _$controller_, _$rootScope_) {
scope = _$rootScope_.$new();
redux = _$ngRedux_;
var scopeData = {
data : {"test":"stuff"},
person : {"name":"thatDude"}
} ;
scope.$digest();
controller = _$controller_(MyController, {
$scope: scope,
$ngRedux: redux
}, scopeData);
});
});
});
The idea behind Redux is that most of your controllers have no, or very little logic. The logic will be in action creators, reducers and selectors mostly.
In the example you provide, most of your code is just wiring things.
I personally don't test wiring, because it adds very little value, and those kinds of test are generally very brittle.
With that said, if you want to test your controllers nonetheless you have two options:
Use functions instead of classes for controllers. For most controllers using a class adds no real value. Instead use a function, and isolate the logic you want to test in another pure function. You can then test this function without even needing mocks etc.
If you still want to use classes, you will need to use a stub of ng-redux, (something like this: https://gist.github.com/wbuchwalter/d1448395f0dee9212b70 (it's in TypeScript))
And use it like this:
let myState = {
someProp: 'someValue'
}
let ngReduxStub;
let myController;
beforeEach(angular.mock.inject($injector => {
ngReduxStub = new NgReduxStub();
//how the state should be initially
ngReduxStub.push(myState);
myController = new MyController(ngReduxStub, $injector.get('someOtherService');
}));
Note: I personnaly don't use mapDispatchToProps (I expose my action creators through an angular service), so the stub does not handle actions, but it should be easy to add.
I have been getting the following error while trying to write tests using Karma and Jasmine:
TypeError: undefined is not a constructor (evaluating
$ngRedux.connect(function (state) { ({}, state.editAudience); }, _actions2.default)')
I managed to make it work by roughly translating wbuch's stub into es6 and using it like this:
angular.mock.module( $provide => {
ngReduxStub = new NgReduxStub()
ngReduxStub.push(myState)
const dependencies = ['$scope', '$ngRedux']
dependencies.forEach( dependency => {
if(dependency =='$ngRedux') { return $provide.value('$ngRedux', ngReduxStub)}
return $provide.value(dependency, {})
})
})
angular.mock.inject( ($compile, $rootScope, $componentController) => {
scope = $rootScope.$new()
ctrl = $componentController('csEditAudience', {$scope: scope}, {})
})
Here's my version of the stub
I hope this helps!
I'm currently getting started with angular unit testing. As the first controller I wanted to tes looked like this, I got confused.
angular.module('sgmPaperApp')
.controller('AccountCtrl', function ($mdToast, user, $firebaseArray, Ref) {
var vm = this;
vm.data = user;
vm.save = saveUser;
vm.comments = $firebaseArray(Ref.child('comments').orderByChild('person').equalTo(user.$id));
function saveUser() {
vm.data.$save().then(function () {
$mdToast.showSimple('Data saved');
});
}
});
Should I really mock all external services I use? After all that controller isn't very much more then external services and mocking the firebaseArray could be difficult.
Thanks for your advice and helping me get started with testing
You don't need to worry about what the external dependencies do, just mock their APIs.
These are the only mocks I can see. I'm going to assume you're using Jasmine
var Ref, $firebaseArray, $mdToast, user, vm;
beforeEach(function() {
Ref = jasmine.createSpyObj('Ref', ['child', 'orderByChild', 'equalTo']);
Ref.child.and.returnValue(Ref);
Ref.orderByChild.and.returnValue(Ref);
Ref.equalTo.and.returnValue(Ref);
$firebaseArray = jasmine.createSpy('$firebaseArray').and.returnValue('comments');
$mdToast = jasmine.createSpyObj('$mdToast', ['showSimple']);
user = jasmine.createSpyObj('user', ['$save']);
user.$id = 'id';
module('sgmPaperApp'); // you should consider separate modules per "thing"
inject(function($controller) {
vm = $controller('AccountCtrl', {
$mdToast: $mdToast,
user: user,
$firebaseArray: $firebaseArray,
Ref: Ref
});
});
});
Then you can easily create your tests
it('assigns a bunch of stuff on creation', function() {
expect(vm.data).toBe(user);
expect(vm.comments).toEqual('comments'); // that's what the mock returns
expect(Ref.child).toHaveBeenCalledWith('comments');
expect(Ref.orderByChild).toHaveBeenCalledWith('person');
expect(Ref.equalTo).toHaveBeenCalledWith(user.$id);
expect($firebaseArray).toHaveBeenCalledWith(Ref);
});
You can even test promise based methods like saveUser
it('saves the user and makes some toast', inject(function($q, $rootScope) {
user.$save.and.returnValue($q.when()); // an empty, resolved promise
vm.saveUser();
expect(user.$save).toHaveBeenCalled();
expect($mdToast.showSimple).not.toHaveBeenCalled(); // because the promise hasn't resolved yet
$rootScope.$apply(); // resolves promises
expect($mdToast.showSimple).toHaveBeenCalledWith('Data saved');
}));
So to answer the question we need to consider what we're actually trying to do. If we are trying to unit test, then yes, we need to mock all dependencies.
Mocking your dependencies won't be hard though. You only need to mock what you're using.
For example, $firebaseArray starts off as a function that receives a paramter, we know that much:
var mockFirebaseArray = function(ref) {
};
Next, before we can finish it, we need to mock the Ref:
var mockRef = {
child: function(path) {
this.orderByChild = function(path) {
this.equalTo = function(val) {
};
return this;
};
return this;
}
};
With these things in place we can decide how the test will "pass". We could just use spies. Or, we could set local variables that we can assert later on our way through.
Spies are my preferred method because you can even verify they were called with specific values:
expect(mockFirebaseArray).toHaveBeenCalled();
expect(mockRef.child).toHaveBeenCalledWith('comments');
Now, if you're wanting to write an integration test that's different. In that case I'd still use spies, but you'd actually be executing those dependencies. Generally speaking there is no need to test your dependencies because they should be tested in isolation as well. Furthermore, there is less need to test other people's API's if they are from trustworthy sources.
I am running into a race condition using angular-translate-loader-partial.
First, I change the state of the loader using $translatePartialLoader.addPart(partname), which requires the table to then be updated. See API for details
Next, I call $translate(translateKey). This begins a race. It may or may not display the translation, depending on whether or not the language file has been loaded yet.
How can I ensure that my partial files are all loaded before I use $translate?
One option would be to hook the $translatePartialLoaderStructureChanged event. Which will only be triggered once the part is avaialable.
To make sure that the part is avaialable:
$rootScope.$on('$translatePartialLoaderStructureChanged', function (e, Part_Name) {
if (Part_Name === 'PartThatYouNeed') {
$translate.refresh().then(function () {
$translate(translateKey);
});
}
});
Or perhaps a better option if this is to be used throughout your application, you can hook this event and call the refresh function in your applications run command like this
app.run(function ($state, $rootScope, $translate) {
$rootScope.$on('$translatePartialLoaderStructureChanged', function (e, Part_Name) {
$translate.refresh();
});
});
And if you need to run setup code after the language table is accessible I would suggest a rootScope emit after the refresh is complete:
app.run(function ($state, $rootScope, $translate) {
$rootScope.$on('$translatePartialLoaderStructureChanged', function (e, Part_Name) {
$translate.refresh().then(function () {
$rootScope.$emit('ApplicationTranslationsRefreshed', Part_Name);
});
});
});
After this anywhere in your app you can hook the ApplicationTranslationsRefreshed event to know when $translate('key_that_is_in_the_new_part') is useable:
$rootScope.$on('ApplicationTranslationsRefreshed', function (e, Part_Name) {
console.debug(Part_Name + "is now avaialable")
});
I have a decorator in Angular that is going to extend the functionality of the $log service and I would like to test it, but I don't see a way to do this. Here is a stub of my decorator:
angular.module('myApp')
.config(function ($provide) {
$provide.decorator('$log', ['$delegate', function($delegate) {
var _debug = $delegate.debug;
$delegate.debug = function() {
var args = [].slice.call(arguments);
// Do some custom stuff
window.console.info('inside delegated method!');
_debug.apply(null, args);
};
return $delegate
}]);
});
Notice that this basically overrides the $log.debug() method, then calls it after doing some custom stuff. In my app this works and I see the 'inside delegated method!' message in the console. But in my test I do not get that output.
How can I test my decorator functionality??
Specifically, how can I inject my decorator such that it actually decorates my $log mock implementation (see below)?
Here is my current test (mocha/chai, but that isn't really relevant):
describe('Log Decorator', function () {
var MockNativeLog;
beforeEach(function() {
MockNativeLog = {
debug: chai.spy(function() { window.console.log("\nmock debug call\n"); })
};
});
beforeEach(angular.mock.module('myApp'));
beforeEach(function() {
angular.mock.module(function ($provide) {
$provide.value('$log', MockNativeLog);
});
});
describe('The logger', function() {
it('should go through the delegate', inject(function($log) {
// this calls my mock (above), but NOT the $log decorator
// how do I get the decorator to delegate the $log module??
$log.debug();
MockNativeLog.debug.should.have.been.called(1);
}));
});
});
From the attached plunk (http://j.mp/1p8AcLT), the initial version is the (mostly) untouched code provided by #jakerella (minor adjustments for syntax). I tried to use the same dependencies I could derive from the original post. Note tests.js:12-14:
angular.mock.module(function ($provide) {
$provide.value('$log', MockNativeLog);
});
This completely overrides the native $log Service, as you might expect, with the MockNativeLog implementation provided at the beginning of the tests because angular.mock.module(fn) acts as a config function for the mock module. Since the config functions execute in FIFO order, this function clobbers the decorated $log Service.
One solution is to re-apply the decorator inside that config function, as you can see from version 2 of the plunk (permalink would be nice, Plunker), tests.js:12-18:
angular.mock.module('myApp', function ($injector, $provide) {
// This replaces the native $log service with MockNativeLog...
$provide.value('$log', MockNativeLog);
// This decorates MockNativeLog, which _replaces_ MockNativeLog.debug...
$provide.decorator('$log', logDecorator);
});
That's not enough, however. The decorator #jakerella defines replaces the debug method of the $log service, causing the later call to MockNativeLog.debug.should.be.called(1) to fail. The method MockNativeLog.debug is no longer a spy provided by chai.spy, so the matchers won't work.
Instead, note that I created an additional spy in tests.js:2-8:
var MockNativeLog, MockDebug;
beforeEach(function () {
MockNativeLog = {
debug: MockDebug = chai.spy(function () {
window.console.log("\nmock debug call\n");
})
};
});
That code could be easier to read:
MockDebug = chai.spy(function () {
window.console.log("\nmock debug call\n");
});
MockNativeLog = {
debug: MockDebug
};
And this still doesn't represent a good testing outcome, just a sanity check. That's a relief after banging your head against the "why don't this work" question for a few hours.
Note that I additionally refactored the decorator function into the global scope so that I could use it in tests.js without having to redefine it. Better would be to refactor into a proper Service with $provider.value(), but that task has been left as an exercise for the student... Or someone less lazy than myself. :D