Unit testing angular 5 component with @ViewChild
Asked Answered
N

3

40

I am using angular 5.2.0. I have a child component

import { Component } from '@angular/core';

@Component({
  template: `<div><div></div></div>`,
})
export class ChildComponent {

  public childMethod() {
    ...
  }
}

and a parent component which accesses the child via ViewChild

import { Component, ViewChild } from "@angular/core";
import { ChildComponent } from "child.component";

@Component({
  template: `
  <child-component #child>
    <child-component></child-component>
  </child-component>
  `,
})
export class ParentComponent {
  @ViewChild("child") child: ChildComponent;

  public parentMethod() {
    this.child.childMethod();
  }
}

I want a unit test proving that an invocation of parentMethod causes an invocation of childMethod. I have the following:


import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ChildComponent } from "./child.component";
import { ParentComponent } from "./parent.component";

describe("ParentComponent", () => {
  let component: Parentcomponent;
  let fixture: ComponentFixture<Parentcomponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ParentComponent, ChildComponent],
      schemas: [NO_ERRORS_SCHEMA],
    }).compileComponents();
  });

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

  it("should invoke childMethod when parentMethod is invoked", () => {
    const childMethodSpy: jasmine.Spy = spyOn(component.child, "childMethod");
    component.parentMethod();
    expect(childMethodSpy).toHaveBeenCalled();
  });
});

Yet, this does not work, and I get Error: <spyOn> : could not find an object to spy upon for childMethod().

Moreover, this is not a unit test, because I use the real ChildComponent instead of a mock. I tried creating a MockChildComponent and adding it to declarations and export but I got the same result. Any help?

I know there are similar post, but they are for different versions of angular, and they did not help.

Namnama answered 25/7, 2018 at 9:32 Comment(1)
You should avoid using NO_ERRORS_SCHEMA. If you remove it, it will tell you that the selectors are missing in your example. With the selectors, your example works. see stackblitz.com/edit/angular-testing-ta7mfv Of course you're right though, mocking ChildComponent is the better solution.Mauriac
E
70

You can do something like this.

Create a spy object for the ChildComponent like this.

const childComponent = jasmine.createSpyObj('ChildComponent', ['childMethod']);

Then in the test, set the component's childComponent property to the spy that you have created.

component.childComponent =  childComponent;

Your test file should look like this.

import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ChildComponent } from "./child.component";
import { ParentComponent } from "./parent.component";

describe("ParentComponent", () => {
  let component: ParentComponent;
  let fixture: ComponentFixture<ParentComponent>;

  const childComponent = jasmine.createSpyObj("ChildComponent", [
    "childMethod",
  ]);

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ParentComponent, ChildComponent],
      schemas: [NO_ERRORS_SCHEMA],
    }).compileComponents();
  });

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

  it("should invoke childMethod when parentMethod is invoked", () => {
    component.childComponent = childComponent;
    component.parentMethod();
    expect(childComponent.childMethod).toHaveBeenCalled();
  });
});


Elephantiasis answered 25/7, 2018 at 9:49 Comment(8)
Thanks mate, this worked perfectly! I did not think about using createSpyObj, that was the key!Namnama
If this answer really helped you please mark it as the correct one. Thanks mateElephantiasis
Actually, with you suggestion I do not even have to import ChildComponent and add it to the declarations. Thanks againNamnama
Yes , Remeber unit testing is Testing a single unit. So you dont have to test or declare the real child component. And as you are not testing the template you can just create a spy for the child component.Elephantiasis
how do you that for @viewChildren? @Anuradha GunasekaraGaiseric
How to do same using jest ??Tricolor
I have ViewChild in component and I am using TestHostComponent, how would you mock that viewchild so that it does not go inside html file and give this error 'zone-evergreen.js:171 Uncaught TypeError: Cannot read property 'nativeElement' of undefined'Haber
What if the child component reference was found in ngOnInit, how to inject the mock in this case ? i.e there is no component.parentMethod(), instead the ngOninit would be called from TestBed.createComponent(parentComponent) itself.Alliterative
J
3

Alternatively, quick and easy way of covering such code....

if the .ts file is like:

@ViewChild('datePicker') myDatePicker: AngularMyDatePickerDirective

then in spec.ts file inside it() block you can add something like:

component.myDatePicker = { toggleCalender: () => Promise.resolve()} as unknown as AngularMyDatePickerDirective;
Jackson answered 3/2, 2022 at 6:1 Comment(0)
S
2

You can mock ChildComponent, so you can test ParentComponent in isolation and don't need to assign ChildComponent manually.

This is required if ParentComponent.childComponent is private and it is almost required if ChildComponent has many dependencies.

Your test file should look like this:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ChildComponent } from './child.component';
import { ParentComponent } from './parent.component';

const childMethodSpỳ = jasmine.createSpy('childMethod');
class ChildStubComponent {
  childMethod = childMethodSpỳ;
}

@Component({
  selector: 'child-component',
  template: '',
  providers: [{ provide: ChildComponent, useClass: ChildStubComponent }]
})

describe('ParentComponent', () => {S
  let component: ParentComponent;
  let fixture: ComponentFixture<ParentComponent>;

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

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

  it('should invoke childMethod when parentMethod is invoked', () => {
    component.parentMethod();
    expect(childMethodSpỳ).toHaveBeenCalled();
  });
});

Credits: Idea from Angular inDepth. Based on @Anuradha Gunasekara's answer.

Slender answered 2/9, 2021 at 15:57 Comment(2)
component.childComponent = childComponent; How do you do this when you started saying the child component is private?Domesticity
@ΕГИІИО You don't. In the example code, the ChildStubComponent is set as a provider for ChildComponent, which makes the TestBed provide that stub into the private child attribute instead of the real one. You can then just measure changes on that.Fauver

© 2022 - 2024 — McMap. All rights reserved.