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

Posted

技术标签:

【中文标题】Angular 2 单元测试组件,模拟 ContentChildren【英文标题】:Angular 2 unit testing component, mocking ContentChildren 【发布时间】:2016-11-23 14:21:38 【问题描述】:

我正在 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 文件包含单元测试。

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

【问题讨论】:

【参考方案1】:

感谢drewmoore 在this 问题中的回答,我已经能够正常工作了。

关键是创建一个用于测试的包装器组件,它在其模板中指定向导和向导步骤。然后 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);
    );
  );

如果您有更好/其他的解决方案,也非常欢迎您添加您的答案。我会把这个问题留待一段时间。

【讨论】:

我遇到了类似的问题并遇到了您的问题。在我自己搜索了一些之后,我发现了一篇文章,他们说你现在可以使用 TestBed.createComponent(TypeOfComponent) 使用 @angular/core/testing 的 TestBed 类来完成所有这些工作。也许这可能对其他有同样问题的人有所帮助。 我只想强调关键部分,即 SUT (wizardComponentInstance) 参考。 我尝试了完全相同的方法。但是,我在组件中的步骤仍然是空的。有什么想法吗?【参考方案2】:
    @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();
            );
        );
);

【讨论】:

不鼓励仅使用代码回答,因为它们没有为未来的读者提供太多信息,请对您所写的内容提供一些解释【参考方案3】:

对于最近提出这个问题的任何人,情况都发生了轻微的变化,并且有一种不同的方法可以做到这一点,我觉得这更容易一些。它是不同的,因为它使用模板引用和@ViewChild 来访问被测组件,而不是fixture.debugElement.children[0].componentInstance。此外,语法也发生了变化。

假设我们有一个选择组件,需要传入一个选项模板。如果没有提供该选项模板,我们想测试我们的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!');
  );

);

【讨论】:

感谢发帖。我想你在哪里有TestComponent,你的意思是WrapperComponent。另外,我必须在组件分配后添加fixture.detectChanges() (ref)

以上是关于Angular 2 单元测试组件,模拟 ContentChildren的主要内容,如果未能解决你的问题,请参考以下文章

如何在单元测试中模拟 AngularFire 2 服务?

Angular - 组件中订阅功能的单元测试

如何在 Angular 单元测试中模拟/监视导入的函数

Angular - 测试组件 - 从模拟返回 Promise.reject 时出错

Angular 6 - 如何在单元测试中模拟 router.events url 响应

模拟指令以测试组件 - Angular 8 与 jasmine 和 Karma