Angular 2单元测试组件,模拟ContentChildren

时间:2016-07-20 11:13:38

标签: unit-testing typescript angular

我正在Angular 2 RC4中实现一个向导组件,现在我正在尝试编写som单元测试。 Angular 2中的单元测试开始得到很好的记录,但我根本无法找到如何在组件中模拟内容查询的结果

该应用程序有2个组件(除应用程序组件外),WizardComponent和WizardStepComponent。应用程序组件(app.ts)定义向导及其模板中的步骤:

 <div>
  <fa-wizard>
    <fa-wizard-step stepTitle="First step">step 1 content</fa-wizard-step>
    <fa-wizard-step stepTitle="Second step">step 2 content</fa-wizard-step>
    <fa-wizard-step stepTitle="Third step">step 3 content</fa-wizard-step>
  </fa-wizard>
</div>

WizardComponent(wizard-component.ts)通过使用ContentChildren查询获取对步骤的引用。

@Component({
selector: 'fa-wizard',
template: `<div *ngFor="let step of steps">
            <ng-content></ng-content>
          </div>
          <div><button (click)="cycleSteps()">Cycle steps</button></div>`

})
export class WizardComponent implements AfterContentInit {
    @ContentChildren(WizardStepComponent) steps: QueryList<WizardStepComponent>;
....
}

问题是如何在单元测试中模拟steps变量:

describe('Wizard component', () => {
  it('should set first step active on init', async(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    return tcb
    .createAsync(WizardComponent)
    .then( (fixture) =>{
        let nativeElement = fixture.nativeElement;
        let testComponent: WizardComponent = fixture.componentInstance;

        //how to initialize testComponent.steps with mock data?

        fixture.detectChanges();

        expect(fixture.componentInstance.steps[0].active).toBe(true);
    });
  })));
});

我创建了一个plunker,实现了一个非常简单的向导来演示问题。 wizard-component.spec.ts文件包含单元测试。

如果有人能指出我正确的方向,我将非常感激。

3 个答案:

答案 0 :(得分:18)

感谢drewmoorethis问题中的答案,我已经能够解决这个问题。

关键是创建一个用于测试的包装器组件,它指定向导和向导在其模板中的步骤。然后,Angular将为您执行内容查询并填充变量。

编辑:实施适用于Angular 6.0.0-beta.3

我的完整测试实现如下:

  //We need to wrap the WizardComponent in this component when testing, to have the wizard steps initialized
  @Component({
    selector: 'test-cmp',
    template: `<fa-wizard>
        <fa-wizard-step stepTitle="step1"></fa-wizard-step>
        <fa-wizard-step stepTitle="step2"></fa-wizard-step>
    </fa-wizard>`,
  })
  class TestWrapperComponent { }

  describe('Wizard component', () => {
    let component: WizardComponent;
    let fixture: ComponentFixture<TestWrapperComponent>;

    beforeEach(async(() => {
      TestBed.configureTestingModule({
        schemas: [ NO_ERRORS_SCHEMA ],
        declarations: [
          TestWrapperComponent,
          WizardComponent,
          WizardStepComponent
        ],
      }).compileComponents();
    }));

    beforeEach(() => {
      fixture = TestBed.createComponent(TestWrapperComponent);
      component = fixture.debugElement.children[0].componentInstance;
    });

    it('should set first step active on init', () => {
      expect(component.steps[0].active).toBe(true);
      expect(component.steps.length).toBe(3);
    });
  });

如果您有更好的/其他解决方案,我们也非常欢迎您添加答案。我将这个问题暂时搁置一段时间。

答案 1 :(得分:4)

对于最近提出这个问题的人来说,事情已经略有改变,并且有一种不同的方法可以做到这一点,我觉得这有点容易。它是不同的,因为它使用模板引用和@ViewChild来访问被测组件而不是fixture.debugElement.children[0].componentInstance。此外,语法已更改。

假设我们有一个需要传入选项模板的select组件。我们想测试如果没有提供该选项模板,我们的ngAfterContentInit方法会抛出错误。

以下是该组件的最小版本:

@Component({
  selector: 'my-select',
  template: `
    <div>
      <ng-template
        *ngFor="let option of options"
        [ngTemplateOutlet]="optionTemplate"
        [ngOutletContext]="{$implicit: option}">
      </ng-template>
    </div>
  `
})
export class MySelectComponent<T> implements AfterContentInit {
  @Input() options: T[];
  @ContentChild('option') optionTemplate: TemplateRef<any>;

  ngAfterContentInit() {
    if (!this.optionTemplate) {
      throw new Error('Missing option template!');
    }
  }
}

首先,创建一个包含被测组件的WrapperComponent,如下所示:

@Component({
  template: `
    <my-select [options]="[1, 2, 3]">
      <ng-template #option let-number>
        <p>{{ number }}</p>
      </ng-template>
    </my-select>
  `
})
class WrapperComponent {
  @ViewChild(MySelectComponent) mySelect: MySelectComponent<number>;
}

请注意在测试组件中使用@ViewChild装饰器。这样就可以通过名称访问MySelectComponent作为TestComponent类的属性。然后在测试设置中,声明TestComponentMySelectComponent

describe('MySelectComponent', () => {
  let component: MySelectComponent<number>;
  let fixture: ComponentFixture<WrapperComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      /* 
         Declare both the TestComponent and the component you want to 
         test. 
      */
      declarations: [
        TestComponent,
        MySelectComponent
      ]
    })
      .compileComponents();
  }));

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

    /* 
       Access the component you really want to test via the 
       ElementRef property on the WrapperComponent.
    */
    component = fixture.componentInstance.mySelect;
  });

  /*
     Then test the component as normal.
  */
  describe('ngAfterContentInit', () => {
     component.optionTemplate = undefined;
     expect(() => component.ngAfterContentInit())
       .toThrowError('Missing option template!');
  });

});

答案 2 :(得分:-1)

    @Component({
        selector: 'test-cmp',
        template: `<wizard>
                    <wizard-step  [title]="'step1'"></wizard-step>
                    <wizard-step [title]="'step2'"></wizard-step>
                    <wizard-step [title]="'step3'"></wizard-step>
                </wizard>`,
    })
    class TestWrapperComponent {
    }

    describe('Wizard Component', () => {
        let component: WizardComponent;
        let fixture: ComponentFixture<TestWrapperComponent>;
        beforeEach(async(() => {
            TestBed.configureTestingModule({
                imports: [SharedModule],
                schemas: [NO_ERRORS_SCHEMA],
                declarations: [TestWrapperComponent]
            });
        }));

        beforeEach(() => {
            fixture = TestBed.createComponent(TestWrapperComponent);
            component = fixture.debugElement.children[0].componentInstance;
            fixture.detectChanges();
        });

        describe('Wizard component', () => {
            it('Should create wizard', () => {
                expect(component).toBeTruthy();
            });
        });
});