angularjs - 如何使用 ui-router 1.0.x 测试转换钩子(Hook)

标签 angularjs angular-ui-router jasmine

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

所以在我的一些 Controller 中我现在有这样的东西:

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

在 Controller 的单元测试中,之前我会在 $rootScope 上广播状态更改事件,并使用某些状态名称作为事件参数来满足我需要测试的条件。

例如

$rootScope.$broadcast('$stateChangeStart', {name: 'other-state'}, {}, {}, {});

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

我尝试在测试中仅调用 $state.go('some-state-name') 但我永远无法在转换钩子(Hook)回调函数中命中我自己的逻辑。根据文档here ,以编程方式调用 state.go 应该会触发转换,除非我误读了?

还有其他人设法在适用于新 ui-router 1.0.x 的 Controller 中进行转换 Hook 的单元测试吗?

使用转换钩子(Hook)的 Controller 代码的完整示例:

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');
      });
    });

我的 spy 永远不会被击中,当在本地运行应用程序时,这个钩子(Hook)确实会按预期被调用,所以它一定是我在测试中设置不正确的东西。因为我挂接到 onSuccess 事件,所以我需要额外的东西才能在测试中成功转换吗?

谢谢

更新

我在 gitter 的 ui-router room 中提出了这个问题,其中一位 repo 贡献者回到我身边,建议我在测试中检查对 $state.go('home') 的调用通过在我的测试规范中添加 expect($state.current.name).toBe('home'); 来运行。

这在我的测试中确实通过了,但我仍然无法在转换钩子(Hook)回调中调用我的函数:

enter image description here

除了安装 polyfill 之外,我不确定如何继续此操作对于旧的 $stateChange 事件,这样我可以使用以前的代码,但我宁愿不这样做并找出测试 $transition Hook 的正确方法。

更新2

按照 estus 的回答,我现在已经删除了 $transitions 服务,并将我的转换 Hook 处理程序重构为 Controller 中的私有(private)命名函数:

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

的命名钩子(Hook)回调 onTransitionsSuccess 函数中达到我的内部逻辑

enter image description here

我在这里做错了什么?

更新3

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

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% 的覆盖率:)

enter image description here

希望这能为其他可能正在努力弄清楚如何测试自己的转换钩子(Hook)的人提供一个很好的引用。

最佳答案

AngularJS 路由的一个好的单元测试策略是完全 stub 路由器。真正的路由器会阻止单元被有效地测试,并提供不必要的移动部件和意外的行为。由于 ngMock 的行为与实际应用程序不同,因此此类测试 z 也不能被视为正确的集成测试。

所有正在使用的路由器服务都应该被 stub 。 $stateProvider stub 应反射(reflect)其基本行为,即它应在 state 调用时返回自身,并应在 $get 上返回 $state stub 调用:

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);
}));

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

this.onTransitionStart = (transition) => { ... };
this.$transitions.onStart({ }, this.onTransitionStart);

然后可以测试 stub 方法是否使用正确的参数调用:

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

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

关于angularjs - 如何使用 ui-router 1.0.x 测试转换钩子(Hook),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46975586/

相关文章:

javascript - HTML 单元由于某种原因无法找到表单。我怀疑是因为 angularJS 但不确定

javascript - 显示/隐藏搜索 Div Angular UI

javascript - AngularJS 选择语义 UI 下拉列表

javascript - AngularJS $state.go 传递的参数始终为空值

css - ionic 中带有 ng-repeat 的列不会分成行

javascript - javascript CustomEvent 的 jasmine 单元测试

angularjs - 如何在AngularJS指令模板中动态定义要用ng-click调用的函数

AngularJS:无法获取指令中隔离范围属性的访问值

javascript - 如何解析模拟函数中的 Promise?

angularjs - Jasmine 测试 : How to mock a method inside a controller in AngularJS