Angular2:如何测试有时 returns,有时路由的组件功能?

Angular2: How to test a component function that sometimes returns, sometimes routes?

我仍在开发基于 Angular2 Heroes 教程的应用程序。在这一点上,我有一个组件,用户可以在其中进行编辑,单击“保存”,成功后用户将进入父页面(路由“../”)。如果保存出错则不会进行路由,页面会显示错误信息。

精灵出现在组件的保存函数中:

private gotoParent(): void {
  this.router.navigate(['../'], { relativeTo: this.route });
}

public save(): void {
  this.error = null;
  let that = this;

  this.orgService
      .save(that.org)
      .subscribe(
          (org: Org): void => {
            that.org = org; 
            that.savedOrg = new Org(that.org);
            that.gotoParent();
        },
        error => this.error = error
      );

}

目前我的测试是:

routeStub = { data: Observable.of( { org: org1 } ), snapshot: {} };

TestBed.configureTestingModule({
    imports: [ FormsModule, RouterTestingModule ],
    providers : [
        { provide: DialogService, useClass: MockDialogService },
        { provide: GlobalsService, useClass: MockGlobalsService },
        { provide: OrgService, useClass: MockOrgService },
        { provide: ActivatedRoute, useValue: routeStub }          
    ],
    declarations: [ OrgDetailComponent ],
  })
  .compileComponents();
}));

...

it('responds to the Save click by saving the Org and refilling the component', async(() => {
  fixture.detectChanges();
  fixture.whenStable().then(() => {
    comp = fixture.componentInstance;
    comp.org = new Org(org1);
    comp.org.id = 2;
    comp.org.name = 'Another Org';

    let elButton = fixture.debugElement.query(By.css('#save'));
    elButton.nativeElement.click();

    fixture.detectChanges();
    fixture.whenStable().then(() => {
      expect(comp.error).toBeNull();
      expect(comp.savedOrg.id).toEqual(2);
      expect(comp.savedOrg.name).toEqual('Another Org');
      expect(routeStub).toHaveBeenCalledWith(['../']);
    });
  });    

}));

当调用 expect(routeStub) 时,我得到 "Error: expected a spy, but got Object ...".

大多数关于测试路由的教程都会设置一个路由 table 并对其进行测试。我不确定我是否需要路由 class(替换 ActivatedRoute?)。

谢谢,

杰罗姆。

3 月 25 日更新

snorkpete 的回答以及 peeskillet 在其他主题中的回答没有解决我的问题。我认为这是因为我的代码中发生了两件不同的事情,而我在这里只分享了一件。

我的组件有一个 ngOnInit(),它依赖解析器将数据传送到 ngOnInit() 中的 subscribe()。在我的测试中,这是由(重命名的)activatedRouteStub 实例提供的:

activatedRouteStub = { data: Observable.of( { org: org1 } ) }

在测试 ngOnInit() 时,我得到了提供的 Org 对象。

现在我还需要处理一个保存按钮,这也会导致浏览器显示父页面。组件调用:

this.router.navigate(['../'], {relativeTo: this.route});

如果我删除 activatedRouteStub,用 routerStub 替换它,一切都会崩溃。

如果我同时使用 activatedRouteStub 和 routerStub,调用

expect(routerStub.navigate).toHaveBeenCalled()

失败,抱怨期待一个间谍并得到一个对象。

如果我 添加 导航:jasmineCreateSpy('navigate') 到 activatedRouteStub 并在 activatedRouteStub.navigate() 上执行 expect() 我被告知那个没有被导航到。

我很纳闷

杰罗姆。

3 月 25 日的解决方案,17:00 CDT

多亏了 peeskillet 的事先帮助和 snorkpete 的即时帮助,我的问题得到了解答。

我恰好同时需要 ActivatedRoute 和路由器。更重要的是,当我调用 toHaveBeenCalledWith() 时,我需要提供 this.router.navigate() 调用所提供的所有内容。 "DUH" 我的观察,但没有意识到它浪费了我大量的时间。

为了将完整的解决方案整合到一个地方,这里是我的组件及其测试规范的相关代码。

对于组件:

public ngOnInit(): void {
  this.error = null;
  this.stateOptions = this.globalsService.getStateOptions();
  let that = this;

  this.route.data
    .subscribe((data: { org: Org }) => {
      that.org = data.org;
      that.savedOrg = new Org(that.org);
    });
}

private gotoParent(): void {
  this.router.navigate(['../'], { relativeTo: this.route });
}

public save(): void {
  this.error = null;
  let that = this;

  this.orgService
      .save(that.org)
      .subscribe(
          (org: Org): void => {
            that.org = org; 
            that.savedOrg = new Org(that.org);
            that.gotoParent();
        },
        error => this.error = error
      );

}

请注意,goToParent() 使用路由字符串和 relativeTo: 参数。

测试中:

@Injectable()
export class MockActivatedRoute {
  constructor() { }

  data: Observable<Org> = null;
}

@Injectable()
export class MockRouter {
  constructor() { }

  navigate: any = () => {};

  snapshot: any = {};
}

describe("...", () => {

  ...

  let router: Router = null;
  let activatedRoute: ActivatedRoute = null;

  beforeEach(async(() => {

    TestBed.configureTestingModule({
      imports: [ FormsModule, RouterTestingModule ],
      providers : [
        { provide: DialogService, useClass: MockDialogService },  // don't worry about these three in this example...
        { provide: GlobalsService, useClass: MockGlobalsService },
        { provide: OrgService, useClass: MockOrgService },
        { provide: Router, useClass: MockRouter },
        { provide: ActivatedRoute, useClass: MockActivatedRoute }          
      ],
      declarations: [ OrgDetailComponent ],
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(OrgDetailComponent);

    dialogService = fixture.debugElement.injector.get(DialogService);
    globalsService = fixture.debugElement.injector.get(GlobalsService);
    orgService = fixture.debugElement.injector.get(OrgService);
    router = fixture.debugElement.injector.get(Router);
    activatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
  });

  it('responds to the Save click by saving the Org and refilling the component', async(() => {
    activatedRoute.data = Observable.of( { org: org1 } ); // The org1 is an instance of Org.
    let spy = spyOn(router, 'navigate');
    fixture.detectChanges();
    fixture.whenStable().then(() => {
      comp = fixture.componentInstance;
      comp.org = new Org(org1);
      comp.org.id = 2;
      comp.org.name = 'Another Org';

      let elButton = fixture.debugElement.query(By.css('#save'));
      elButton.triggerEventHandler('click', null);

      fixture.detectChanges();
      fixture.whenStable().then(() => {
        expect(comp.error).toBeNull();
        expect(comp.savedOrg.id).toEqual(2);
        expect(comp.savedOrg.name).toEqual('Another Org');
        expect(router.navigate).toHaveBeenCalled();
        expect(router.navigate).toHaveBeenCalledWith(['../'], { relativeTo: activatedRoute });
      });
    });    

  }));

});

在你的例子中,你存根错误。处理您正在尝试做的事情的最简单方法是意识到您的组件依赖于路由器服务。 (记住,它调用 router.navigate)。这是您要用 mock/stub 对象替换的 'complicated dependency'。

因此,您应该更改测试模块中的提供程序列表,以便为路由器提供一个存根,returns 一个具有导航方法的虚拟对象。然后您可以确认存根中的导航方法是否在您期望调用时被调用。

 providers : [
        { provide: DialogService, useClass: MockDialogService },
        { provide: GlobalsService, useClass: MockGlobalsService },
        { provide: OrgService, useClass: MockOrgService },
        //{ provide: ActivatedRoute, useValue: routeStub }  <-- remove this   
        { provide: Router, useValue: routerStub }       <-- add this  
    ],

如前所述,您的路由器存根是一个虚拟对象,上面只有一个 'navigate' 方法。您必须监视该方法。

let fakeRouter = TestBed.get(Router);  // you must retrieve your router fake through dependency injection
spyOn(fakeRouter, 'navigate');

那么在你的测试中,

 expect(fakeRouter.navigate).toHaveBeenCalledWith(['../']);

请注意,您正在监视和测试的 'router' 对象不能是您 es6 导入到测试文件中的 routerStub。您必须确保通过依赖注入检索您的 fakeRouter。

编辑

额外的信息很有帮助 - 您可以使用 routeStub 对 ActivatedRoute 进行存根 - 正如您可能已经意识到的那样,routeStub 被用作从解析器获取数据的替代品。所以那部分工作正常。但是由于您还想确认 router.navigate 方法的调用符合您的预期,因此您还必须对其进行存根。

因此,您的测试模块的提供程序列表应包含:

providers : [
  { provide: DialogService, useClass: MockDialogService },
  { provide: GlobalsService, useClass: MockGlobalsService },
  { provide: OrgService, useClass: MockOrgService },
  { provide: ActivatedRoute, useValue: routeStub }, //<-- to simulate the resolver passing data to your component          
  { provide: Router, useValue: routerStub },  //<-- dummy object with navigate method that you spy on to ensure you navigate when you expect to 
],

如前所述,routerStub 是一个简单的对象,只有一个导航方法,您将监视该方法以查看它是否被正确调用。

所以,在你的测试中,

it('responds to the Save click by saving the Org and refilling the component', async(() => {

  // get an instance of your router from your TestBed.
  // but because of how your providers are configured,
  // when you ask for an instance of Router, TestBed will instead
  // return an instance of your routerStub.
  // You MUST get your routerStub through dependency injection -
  // either using TestBed.get or the inject function or some other means
  let fakeRouter = TestBed.get(Router);


  // This is jasmine at work now.
  // Later on, we want to confirm that the navigate method on
  // our fakeRouter is called, so we tell jasmine to monitor that method
  // Jasmine won't allow that spyOn call to work unless 
  // fakeRouter actually has a navigate method - hence the reason
  // our routerStub needed to implement one
  spyOn(fakeRouter,'navigate');

  fixture.detectChanges();
  fixture.whenStable().then(() => {
    comp = fixture.componentInstance;
    comp.org = new Org(org1);
    comp.org.id = 2;
    comp.org.name = 'Another Org';

    let elButton = fixture.debugElement.query(By.css('#save'));
    elButton.nativeElement.click();

    fixture.detectChanges();
    fixture.whenStable().then(() => {
      expect(comp.error).toBeNull();
      expect(comp.savedOrg.id).toEqual(2);
      expect(comp.savedOrg.name).toEqual('Another Org');

      // we set up our spy on our navigate method above.
      // now, we check that the method in question has actually been called.
      // note that i'm checking the method itself -
      // in spying, jasmine replaces that 'navigate' method 
      // with something else that it can later call assertions with
      // Hence, we check against that property explicitly
      expect(fakeRouter.navigate).toHaveBeenCalledWith(['../']);
    });
  });    

}));