如何使用ui-router 1.0.x测试转换挂钩

min*_*rse 13 jasmine angularjs angular-ui-router

我正在迁移到ui-router的最新稳定版本,并且正在使用$ transitions生命周期钩子在某些状态名称转换到时执行某些逻辑.

所以在我的一些控制器中,我现在有这样的事情:

this.$transitions.onStart({ }, (transition) => {
    if (transition.to().name !== 'some-state-name') {
        //do stuff here...
    }
});
Run Code Online (Sandbox Code Playgroud)

在我对控制器的单元测试中,之前我会在$ rootScope上广播状态更改事件,并将某些状态名称作为事件参数,以达到我需要测试的条件.

例如

$rootScope.$broadcast('$stateChangeStart', {name: 'other-state'}, {}, {}, {});
Run Code Online (Sandbox Code Playgroud)

由于这些状态事件已被弃用,目前$transitions.onStart(...)在测试中触发挂钩的正确方法是什么?

我试过调用$state.go('some-state-name')我的测试,但我永远不会在转换钩子回调函数中遇到我自己的逻辑.根据这里的文档,以编程方式调用state.go应该触发转换,除非我误读?

有没有其他人设法在他们的控制器中为新的ui-router 1.0.x进行转换挂钩的单元测试?

使用转换钩子的我的控制器代码的完整示例:

this.$transitions.onSuccess({ }, (transition) => {
      this.setOpenItemsForState(transition.to().name);
    });
Run Code Online (Sandbox Code Playgroud)

测试规范:

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');
      });
    });
Run Code Online (Sandbox Code Playgroud)

我的间谍永远不会被击中,当在本地运行应用程序时,这个钩子会按预期调用,所以它必须是我在测试中设置不正确的东西.我是否需要一些额外的东西才能使转换在测试中成功,因为我正在挂钩onSuccess事件?

谢谢

UPDATE

我在gi上的ui-router机房中提出了这个问题,其中一个回购贡献者回到我身边,建议我检查$state.go('home')我的测试中的调用实际上是通过添加expect($state.current.name).toBe('home');我的测试规范来运行的.

这在我的测试中确实传递给我,但我仍然无法在转换钩子回调中调用我的函数:

在此输入图像描述

我不确定如何继续这个,除了为传统的$ stateChange事件安装polyfill,所以我可以使用我以前的代码,但我宁愿不这样做,并找出测试$ transition钩子的正确方法.

更新2

按照estus的回答,我现在已经删除了$transitions服务,并且还将我的转换钩子处理程序重构为我的控制器中的私有命名函数:

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...
    }
}
Run Code Online (Sandbox Code Playgroud)

现在在我的测试规范中,我有:

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);
      });
    });
Run Code Online (Sandbox Code Playgroud)

这些期望通过了,但是我仍然无法在我的命名钩子回调onTransitionsSuccess函数中调用我的内部逻辑来调用setOpenItemsForState

在此输入图像描述

我在这做错了什么?

更新3

再次感谢estu,我忘记了我可以只调用我的命名转换钩子函数是一个单独的测试:

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');
        });
      });
Run Code Online (Sandbox Code Playgroud)

现在我得到100%的报道:)

在此输入图像描述

希望这可以作为对其他可能正在努力弄清楚如何测试自己的过渡钩子的人的一个很好的参考.

Est*_*ask 6

AngularJS路由的一个好的单元测试策略是完全存根路由器.真正的路由器可以防止单元被有效测试,并提供不必要的移动部件和意外行为 由于ngMock行为与实际应用不同,因此这些测试也不能被认为是正确的集成测试.

所有正在使用的路由器服务都应该是存根的.$stateProvider存根应该反映它的基本行为,即它应该在state调用时返回并且应该$state$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);
}));
Run Code Online (Sandbox Code Playgroud)

一种测试友好的方法是提供绑定方法作为回调而不是匿名函数:

this.onTransitionStart = (transition) => { ... };
this.$transitions.onStart({ }, this.onTransitionStart);
Run Code Online (Sandbox Code Playgroud)

然后可以测试存根方法,使用适当的参数调用它们:

expect($transitions.onStart).toHaveBeenCalledTimes(1);
$transitions.onStart.mostRecent().args[0].toEqual({});
$transitions.onStart.mostRecent().args[1].toBe(this.onTransitionStart);
Run Code Online (Sandbox Code Playgroud)

可以通过使用预期参数调用回调函数来直接测试它.这提供了全面的覆盖,但却留下了一些人为错误的地方,因此应该使用真实路由器的集成/ e2e测试来备份单元测试.