如何使用 ui-router 1.0.x 测试转换挂钩
How to test transition hooks with ui-router 1.0.x
我正在迁移到 ui-router 的最新稳定版本,并且正在使用 $t运行sitions 生命周期挂钩在某些状态名称被 t运行定位于.
所以在我的一些控制器中我现在有这样的东西:
this.$transitions.onStart({ }, (transition) => {
if (transition.to().name !== 'some-state-name') {
//do stuff here...
}
});
在我的控制器单元测试中,之前我会在 $rootScope 上广播一个状态更改事件,并将特定的状态名称作为事件参数来满足我需要测试的条件。
例如
$rootScope.$broadcast('$stateChangeStart', {name: 'other-state'}, {}, {}, {});
由于不推荐使用这些状态事件,现在在测试中触发 $transitions.onStart(...)
挂钩的正确方法是什么?
我试过在我的测试中只调用 $state.go('some-state-name')
但我永远无法在 t运行sition 挂钩回调函数中达到我自己的逻辑。根据文档 here,以编程方式调用 state.go 应该会触发 t运行sition,除非我误读了?
有没有其他人设法在他们的控制器中为新的 ui-router 1.0.x 工作的 t运行sition 挂钩进行单元测试?
我的控制器代码使用 t运行sition hook 的完整示例:
this.$transitions.onSuccess({ }, (transition) => {
this.setOpenItemsForState(transition.to().name);
});
测试规格:
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');
});
});
我的间谍从未被击中,当 运行 本地应用程序确实按预期调用此挂钩时,所以它一定是我在测试中设置不正确的东西。因为我正在连接到 onSuccess 事件,所以我需要做一些额外的事情才能使 t运行sition 在测试中成功吗?
谢谢
更新
我在 gitter 的 ui-router room 中提出了这个问题,其中一位 repo 贡献者回复我,建议我在测试中检查对 $state.go('home')
的调用实际上 运行通过在我的测试规范中添加 expect($state.current.name).toBe('home');
。
这在我的测试中确实通过了,但我仍然无法在 t运行sition 挂钩回调中调用我的函数:
除了为遗留的 $stateChange 事件安装 polyfill 以便我可以使用我以前的代码之外,我不确定如何继续此操作,但我宁愿不这样做并弄清楚测试 $t运行sition 挂钩的正确方法。
更新 2
根据 estus 的回答,我现在已经删除了 $transitions
服务,并将我的 t运行sition 挂钩处理程序重构为我的控制器中的私有命名函数:
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...
}
}
现在在我的测试规范中我有:
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);
});
});
这些期望通过了,但我仍然无法在调用 setOpenItemsForState
的命名钩子回调 onTransitionsSuccess
函数中达到我的内在逻辑
我做错了什么?
更新 3
再次感谢 estu,我忘记了我只能调用我命名的 t运行sition 挂钩函数是一个单独的测试:
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');
});
});
现在我获得了 100% 的覆盖率:)
希望这对可能正在努力弄清楚如何测试自己的 t运行sition 挂钩的其他人来说是一个很好的参考。
AngularJS 路由的一个很好的单元测试策略是完全对路由器进行存根。真正的路由器会阻止单元被有效测试,并提供不必要的移动部件和意外行为。由于 ngMock
的行为与实际应用程序不同,因此此类测试也不能被视为适当的集成测试。
所有正在使用的路由器服务都应该被存根。 $stateProvider
stub 应该反映它的基本行为,即它应该 return 在 state
调用上并且应该 return $state
stub 在 $get
调用上:
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);
}));
一种测试友好的方法是提供 绑定 方法作为回调而不是匿名函数:
this.onTransitionStart = (transition) => { ... };
this.$transitions.onStart({ }, this.onTransitionStart);
然后可以测试存根方法是否使用正确的参数调用它们:
expect($transitions.onStart).toHaveBeenCalledTimes(1);
$transitions.onStart.mostRecent().args[0].toEqual({});
$transitions.onStart.mostRecent().args[1].toBe(this.onTransitionStart);
可以通过使用预期参数调用它来直接测试回调函数。这提供了完整的覆盖范围,但也为人为错误留下了一些空间,因此单元测试应该使用 integration/e2e 真实路由器测试进行备份。
我正在迁移到 ui-router 的最新稳定版本,并且正在使用 $t运行sitions 生命周期挂钩在某些状态名称被 t运行定位于.
所以在我的一些控制器中我现在有这样的东西:
this.$transitions.onStart({ }, (transition) => {
if (transition.to().name !== 'some-state-name') {
//do stuff here...
}
});
在我的控制器单元测试中,之前我会在 $rootScope 上广播一个状态更改事件,并将特定的状态名称作为事件参数来满足我需要测试的条件。
例如
$rootScope.$broadcast('$stateChangeStart', {name: 'other-state'}, {}, {}, {});
由于不推荐使用这些状态事件,现在在测试中触发 $transitions.onStart(...)
挂钩的正确方法是什么?
我试过在我的测试中只调用 $state.go('some-state-name')
但我永远无法在 t运行sition 挂钩回调函数中达到我自己的逻辑。根据文档 here,以编程方式调用 state.go 应该会触发 t运行sition,除非我误读了?
有没有其他人设法在他们的控制器中为新的 ui-router 1.0.x 工作的 t运行sition 挂钩进行单元测试?
我的控制器代码使用 t运行sition hook 的完整示例:
this.$transitions.onSuccess({ }, (transition) => {
this.setOpenItemsForState(transition.to().name);
});
测试规格:
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');
});
});
我的间谍从未被击中,当 运行 本地应用程序确实按预期调用此挂钩时,所以它一定是我在测试中设置不正确的东西。因为我正在连接到 onSuccess 事件,所以我需要做一些额外的事情才能使 t运行sition 在测试中成功吗?
谢谢
更新
我在 gitter 的 ui-router room 中提出了这个问题,其中一位 repo 贡献者回复我,建议我在测试中检查对 $state.go('home')
的调用实际上 运行通过在我的测试规范中添加 expect($state.current.name).toBe('home');
。
这在我的测试中确实通过了,但我仍然无法在 t运行sition 挂钩回调中调用我的函数:
除了为遗留的 $stateChange 事件安装 polyfill 以便我可以使用我以前的代码之外,我不确定如何继续此操作,但我宁愿不这样做并弄清楚测试 $t运行sition 挂钩的正确方法。
更新 2
根据 estus 的回答,我现在已经删除了 $transitions
服务,并将我的 t运行sition 挂钩处理程序重构为我的控制器中的私有命名函数:
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...
}
}
现在在我的测试规范中我有:
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);
});
});
这些期望通过了,但我仍然无法在调用 setOpenItemsForState
onTransitionsSuccess
函数中达到我的内在逻辑
我做错了什么?
更新 3
再次感谢 estu,我忘记了我只能调用我命名的 t运行sition 挂钩函数是一个单独的测试:
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');
});
});
现在我获得了 100% 的覆盖率:)
希望这对可能正在努力弄清楚如何测试自己的 t运行sition 挂钩的其他人来说是一个很好的参考。
AngularJS 路由的一个很好的单元测试策略是完全对路由器进行存根。真正的路由器会阻止单元被有效测试,并提供不必要的移动部件和意外行为。由于 ngMock
的行为与实际应用程序不同,因此此类测试也不能被视为适当的集成测试。
所有正在使用的路由器服务都应该被存根。 $stateProvider
stub 应该反映它的基本行为,即它应该 return 在 state
调用上并且应该 return $state
stub 在 $get
调用上:
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);
}));
一种测试友好的方法是提供 绑定 方法作为回调而不是匿名函数:
this.onTransitionStart = (transition) => { ... };
this.$transitions.onStart({ }, this.onTransitionStart);
然后可以测试存根方法是否使用正确的参数调用它们:
expect($transitions.onStart).toHaveBeenCalledTimes(1);
$transitions.onStart.mostRecent().args[0].toEqual({});
$transitions.onStart.mostRecent().args[1].toBe(this.onTransitionStart);
可以通过使用预期参数调用它来直接测试回调函数。这提供了完整的覆盖范围,但也为人为错误留下了一些空间,因此单元测试应该使用 integration/e2e 真实路由器测试进行备份。