Search code examples
angularunit-testingmockingangular-content-projection

Angular Test Mock ContentChild


I want to test an Angular Component and replace the contentChild with a Mock. I followed this guide: https://medium.com/angular-in-depth/angular-unit-testing-viewchild-4525e0c7b756 The guide is for a viewChild, but I thought this could also work for a content Child.

My contentChild has a Observable to which the parent subscribe. If I test the code with the real child, it works. If I mock the child, the test doesn't work. I think the child mock, I query in the test to emit a new value, is a different instance, than the one which the parent subscribed to.

My parent:

@Component({
  // tslint:disable-next-line:component-selector
  selector: '[appParent]',
  template: `
    <div>
      <span class="true" *ngIf="display$ | async; else other">True</span>
    </div>
    <ng-content></ng-content>
    <ng-template #other><span class="false">False</span></ng-template>
  `
})
export class ParentComponent implements AfterContentInit {

  @ContentChild(ChildComponent) child!: ChildComponent;

  display$: Observable<boolean>;

  ngAfterContentInit(): void {
    this.display$ = this.child.display$;
  }
}

My mock:

@Component({
  selector: 'app-child',
  template: '',
  providers: [
    {
      provide: ChildComponent,
      useExisting: ChildStubComponent
    }
  ]
})
export class ChildStubComponent {
  displaySubject = new BehaviorSubject(false);
  display$: Observable<boolean> = this.displaySubject.asObservable();
}

And the test:

describe('ParentComponentTest', () => {
  @Component({
    template: `
      <div appParent>
        <app-child></app-child>
      </div>
    `
  })
  class TestComponent {
    @ViewChild(ChildStubComponent)
    child!: ChildStubComponent;
  }

  let component: TestComponent;
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [
        TestComponent,
        ChildStubComponent,
        ParentComponent
      ]
    }).compileComponents();
  });

  beforeEach(async () => {
    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should find true', () => {
    component.child.displaySubject.next(true);
    fixture.detectChanges();
    expect(fixture.debugElement.query(By.css('.true'))).toBeTruthy();
  });

});

Solution

  • The problem seams to be, that you get with the view-child not the correct instance. The solution is to get the instance from the child property of the parent component.

    
    describe('ParentComponentTest', () => {
      @Component({
        template: `
          <div appParent>
            <app-child></app-child>
          </div>
        `
      })
      class TestComponent {
        @ViewChild(ParentComponent)
        parent!: ParentComponent;
      }
    
      let component: TestComponent;
      let fixture: ComponentFixture<TestComponent>;
    
      ....
    
      it('should find true', () => {
        const childStub = component.parent.child.toArray()[0] as unknown as ChildStubComponent;
        childStub.displaySubject.next(true);
        fixture.detectChanges();
        expect(fixture.debugElement.query(By.css('.true'))).toBeTruthy();
      });
    
    });