简体   繁体   中英

Angular 2 unit testing component, mocking ContentChildren

I am implementing a wizard component in Angular 2 RC4, and now I am trying to write som unit tests. Unit testing in Angular 2 is starting to get well documented, but I simply cannot find out how to mock the result of a content query in the component.

The app has 2 components (in addition to the app component), WizardComponent and WizardStepComponent. The app component (app.ts) defines the wizard and the steps in its template:

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

The WizardComponent (wizard-component.ts) gets a reference to the steps by using a ContentChildren query.

@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>;
....
}

The problem is how to mock the steps variable in the unit test:

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

I have created a plunker implementing a very simple wizard demonstrating the problem. The wizard-component.spec.ts file contains the unit test.

If anyone can point me in the right direction, I would greatly appreciate it.

Thanks to drewmoore 's answer in this question, I have been able to get this working.

The key is to create a wrapper component for testing, which specifies the wizard and the wizard steps in it's template. Angular will then do the content query for you and populate the variable.

Edit: Implementation is for Angular 6.0.0-beta.3

My full test implementation looks like this:

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

If you have better/other solutions, you are very welcome to add you answer as well. I'll leave the question open for some time.

For anybody coming to this question recently, things have changed slightly and there is a different way to do this, which I find a bit easier. It is different because it uses a template reference and @ViewChild to access the component under test rather than fixture.debugElement.children[0].componentInstance . Also, the syntax has changed.

Let's say we have a select component that requires an option template to be passed in. And we want to test that our ngAfterContentInit method throws an error if that option template is not provided.

Here is a minimal version of that component:

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

First, create a WrapperComponent which contains the component under test, like so:

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

Note the use of the @ViewChild decorator in the test component. That gives access to MySelectComponent by name as a property on the TestComponent class. Then in the test setup, declare both the TestComponent and the MySelectComponent .

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM