61

I am writing an Angular 2 unit test. I have a @ViewChild subcomponent that I need to recognize after the component initializes. In this case it's a Timepicker component from the ng2-bootstrap library, though the specifics shouldn't matter. After I detectChanges() the subcomponent instance is still undefined.

Pseudo-code:

@Component({
    template: `
        <form>
            <timepicker
                #timepickerChild
                [(ngModel)]="myDate">
            </timepicker>
        </form>
    `
})
export class ExampleComponent implements OnInit {
    @ViewChild('timepickerChild') timepickerChild: TimepickerComponent;
    public myDate = new Date();
}
    
// Spec
describe('Example Test', () => {
    let exampleComponent: ExampleComponent;
    let fixture: ComponentFixture<ExampleComponent>;

    beforeEach(() => {
        TestBed.configureTestingModel({
            // ... whatever needs to be configured
        });
        fixture = TestBed.createComponent(ExampleComponent);
    });

    it('should recognize a timepicker'. async(() => {
        fixture.detectChanges();
        const timepickerChild: Timepicker = fixture.componentInstance.timepickerChild;
        console.log('timepickerChild', timepickerChild)
    }));
});

The pseudo-code works as expected until you reach the console log. The timepickerChild is undefined. Why is this happening?

PaulBunion
  • 346
  • 2
  • 18
ebakunin
  • 3,621
  • 7
  • 30
  • 49
  • 4
    Did you find an answer? I have the same problem. – user3495469 Mar 15 '17 at 15:34
  • 3
    I have a vague feeling like most of the upvoters have a different problem. Make sure that your child component is not hidden by any `*ngIf="false"` directive. Also, after setting the rendering condition to `true`, do a `fixture.detectChanges()` which will (re-)create the previously undefined child component. – Monkey Supersonic Nov 06 '18 at 14:50

7 Answers7

37

I think it should work. Maybe you forgot to import some module in your configuration. Here is the complete code for test:

import { TestBed, ComponentFixture, async } from '@angular/core/testing';

import { Component, DebugElement } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { ExampleComponent } from './test.component';
import { TimepickerModule, TimepickerComponent } from 'ng2-bootstrap/ng2-bootstrap';

describe('Example Test', () => {
  let exampleComponent: ExampleComponent;
  let fixture: ComponentFixture<ExampleComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [FormsModule, TimepickerModule.forRoot()],
      declarations: [
        ExampleComponent
      ]
    });
    fixture = TestBed.createComponent(ExampleComponent);
  });

  it('should recognize a timepicker', async(() => {
    fixture.detectChanges();
    const timepickerChild: TimepickerComponent = fixture.componentInstance.timepickerChild;
    console.log('timepickerChild', timepickerChild);
    expect(timepickerChild).toBeDefined();
  }));
});

Plunker Example

Liam Gray
  • 1,089
  • 9
  • 16
yurzui
  • 205,937
  • 32
  • 433
  • 399
  • If I want to test main component without including actual children, then is there any way to mock them out? – Eugene Feb 16 '19 at 04:25
  • @Eugene Can you provide some example in stackblitz? – yurzui Feb 16 '19 at 04:26
  • But `@ViewChild('timepickerChild') timepickerChild: TimepickerComponent;` should be private field. – NagRock Mar 11 '19 at 14:58
  • Just a note, that null in jasmine is defined and returns true in this comparison. You should do not.toBeNull() instead. – Boat Sep 16 '22 at 08:17
16

Make sure your child component does not have a *ngIf that is evaluating to false. If so, it will result in the child component as being undefined.

Zobair Saleem
  • 161
  • 1
  • 2
4

In most cases just add it to declaration and you are good to go.

beforeEach(async(() => {
        TestBed
            .configureTestingModule({
                imports: [],
                declarations: [TimepickerComponent],
                providers: [],
            })
            .compileComponents() 
Tim
  • 5,435
  • 7
  • 42
  • 62
omri.s
  • 53
  • 1
  • 1
  • Not a good solution in my opinion. If you declare components in your tests, you have a hard dependency to them. Meaning, if they (for some reason) stop working, your test will fail as well. You should rather go for mocking components (like for example here: https://medium.com/@cnunciato/a-simple-mock-component-for-angular-2-d79bd58a7b31) – dave0688 Sep 07 '18 at 10:51
4

If you want to test the main component with a stub child component, you need to add a provider to the stub child component; as explained in the article Angular Unit Testing @ViewChild.

import { Component } from '@angular/core';
import { ChildComponent } from './child.component';

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

Note the providers metadata, to use the class ChildStubComponent when ChildComponent is required.

You can then create your parent component normally, its child will be created with the type ChildStubComponent.

LostMyGlasses
  • 3,074
  • 20
  • 28
arthur.sw
  • 11,052
  • 9
  • 47
  • 104
1

In case you have ViewChild inside and outside of ngIfs, try the following.

First things first, mock your child components, for example with [ng-mocks][1]

import { MockComponent } from 'ng-mocks';
    
beforeEach(async () => {
        await TestBed.configureTestingModule({
                declarations: [ExampleComponent, ...MockComponent(TimepickerComponent)],
                imports: [],
                providers: []
            }).compileComponents;
    
            fixture = TestBed.createComponent(ExampleComponent);
            component = fixture.componentInstance;
    
            fixture.detectChanges();
        });

If your ViewChild IS NOT inside an ngIf, set the static property of the ViewChild to true. This way you have access to it at ngOnInit.

<form>
    <timepicker
         #timepickerChild
         [(ngModel)]="myDate">
    </timepicker>
</form>
@ViewChild('timepickerChild', { static: true }) timepickerChild: TimepickerComponent;

and in your test:

it('should recognize a timepicker', () => {
    console.log('timepickerChild', component.timepickerChild);
    expect(timepickerChild).toBeDefined();
});

If your ViewChild IS inside an ngIf, set the static property of the ViewChild to false (or just don't set it, default is false).

<form>
     <div *ngIf="condition">
        <timepicker
             #timepickerChild
             [(ngModel)]="myDate">
        </timepicker>
     </div>
</form>
@ViewChild('timepickerChild', { static: false}) timepickerChild: TimepickerComponent;

and in your test:

it('should recognize a timepicker', () => {
    component.condition = true;
    fixture.detectChanges();

    console.log('timepickerChild', component.timepickerChild);
    expect(timepickerChild).toBeDefined();
});

If you have a method inside the child component that you need to check if it was called, spy on it and then you can access it:

it('should recognize a timepicker', () => {
     component.condition = true;
     fixture.detectChanges();

     const spy = spyOn(component.timepickerChild, 'childMethod');

     console.log('timepickerChild', component.timepickerChild);
     expect(timepickerChild).toBeDefined();
     expect(spy).toHaveBeenCalled();
});
bokkie
  • 1,477
  • 4
  • 21
  • 40
-1

Even after following everything from the accepted answer, you are getting undefined instance of child component then kindly check if that component is visible.

In my case, there was *ngIf applied on the control that's why instance of child was undefined then I removed and checked and it worked for me

Mohini Mhetre
  • 912
  • 10
  • 29
  • Yes, this [has already been said here](https://stackoverflow.com/a/54894245/542251) – Liam Sep 07 '21 at 14:37
-1

For an alternative way of doing this refer this post:

https://stackoverflow.com/a/70966565/11797105

Luther
  • 149
  • 2
  • 8
  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/late-answers/30976668) – Andrew Halil Feb 07 '22 at 12:31