Angular2:如何测试有时返回的组件函数,有时会路由?

时间:2017-03-23 02:23:18

标签: angular jasmine angular2-routing

我还在开发基于Angular2 Heroes教程的应用程序。此时我有一个用户进行编辑的组件,单击Save并在成功时用户精神充沛到父页面(route" ../")。如果保存时出错,则路由不会发生,页面会显示错误信息。

精神发生在组件的保存功能中:

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(['../']);
    });
  });    

}));

当调用期望(routeStub)时,我得到"错误:期望一个间谍,但得到了对象..."。

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

谢谢,

杰罗姆。

3/25更新

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

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

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

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

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

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

如果我删除了activatedRouteStub,将其替换为routerStub,一切都会中断。

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

expect(routerStub.navigate).toHaveBeenCalled()

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

如果我添加导航:jasmineCreateSpy(&#39;导航&#39;)到activatedRouteStub并在activatedRouteStub.navigate()上执行expect()我告诉< em> 没有导航。

我很困惑。

杰罗姆。

解决方案在3/25,17:00 CDT

感谢peeskillet的事先帮助以及来自snorkpete的直接帮助,我已经回答了我的问题。

我碰巧需要ActivatedRoute和路由器。更重要的是,当我调用toHaveBeenCalledWith()时,我需要提供所有提供this.router.navigate()调用的内容。 A&#34; DUH&#34;对我而言,但没有意识到它浪费了我很多时间。

要在一个地方获得完整的解决方案,这里是我的组件及其测试规范的相关代码。

对于组件:

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

  }));

});

1 个答案:

答案 0 :(得分:1)

在你的例子中,你正在捣乱错误的东西。接近您尝试做的最简单的方法是意识到您的组件依赖于路由器服务。 (记住,它调用router.navigate)。这是一个复杂的依赖关系&#39;你想用mock / stub对象替换它。

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

 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  
    ],

如前所述,您的路由器存根是一个虚拟对象,只有一个导航&#39;方法就可以了。你必须监视那种方法。

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

然后在你的测试中,

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

请注意&#39;路由器&#39;您正在进行间谍活动和测试的对象不能成为您导入测试文件的路由器存根。您必须确保通过依赖注入检索您的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(['../']);
    });
  });    

}));