13

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.

Kjetil Watnedal
  • 6,097
  • 3
  • 29
  • 23

3 Answers3

27

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.

Fabio Picheli
  • 939
  • 11
  • 27
Kjetil Watnedal
  • 6,097
  • 3
  • 29
  • 23
  • 1
    I had a similar issue and came across your question. After searching a little more myself I found a post where they say you can Do all of this now with the TestBed class of @angular/core/testing by using TestBed.createComponent(TypeOfComponent). Maybe this might be a help for other people with the same problem. – Nodon Darkeye Feb 28 '17 at 10:56
  • I just want to highlight the crucial part which is the SUT (`wizardComponentInstance`) reference. – Yuri Dec 13 '17 at 09:07
  • 1
    I tried the exact same way. However, my steps in the component is still empty. Any ideas? – Terence Jul 11 '18 at 03:42
7

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

});
vince
  • 7,808
  • 3
  • 34
  • 41
  • 5
    Thanks for posting. I think where you have `TestComponent` you mean `WrapperComponent`. Also, I had to add `fixture.detectChanges()` after component assigment ([ref](https://angular.io/guide/testing#component-dom-testing)) – Matthew Hegarty Apr 28 '18 at 20:57
-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();
            });
        });
});
Chirag
  • 13
  • 2
  • 1
    Code only answers arent encouraged as they dont provide much information for future readers please provide some explanation to what you have written – WhatsThePoint Oct 05 '17 at 07:13