37

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.

Shadowalker
  • 402
  • 1
  • 7
  • 17
Jacopo Lanzoni
  • 1,264
  • 2
  • 11
  • 25
  • 3
    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 https://stackblitz.com/edit/angular-testing-ta7mfv Of course you're right though, mocking `ChildComponent` is the better solution. – Kim Kern Jul 25 '18 at 09:55

3 Answers3

61

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


Shadowalker
  • 402
  • 1
  • 7
  • 17
Anuradha Gunasekara
  • 6,553
  • 2
  • 27
  • 37
  • 3
    Thanks mate, this worked perfectly! I did not think about using createSpyObj, that was the key! – Jacopo Lanzoni Jul 25 '18 at 09:58
  • 1
    If this answer really helped you please mark it as the correct one. Thanks mate – Anuradha Gunasekara Jul 25 '18 at 10:00
  • Actually, with you suggestion I do not even have to import ChildComponent and add it to the declarations. Thanks again – Jacopo Lanzoni Jul 25 '18 at 10:00
  • 3
    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. – Anuradha Gunasekara Jul 25 '18 at 10:03
  • 1
    how do you that for @viewChildren? @Anuradha Gunasekara – vidhya sagar Jan 21 '20 at 09:51
  • How to do same using jest ?? – Soumya Gangamwar Sep 08 '20 at 12:09
  • 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' – Maddy Mar 05 '21 at 07:51
  • 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. – Sundar Rajan Feb 19 '23 at 18:15
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;
Luther
  • 149
  • 2
  • 8
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.

Eneko
  • 1,709
  • 1
  • 16
  • 25