Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing ngOnChanges lifecycle hook in Angular 2

Given the following code I try to test the ngOnChanges lifecycle hook of Angular2:

import {
    it,
    inject,
    fdescribe,
    beforeEachProviders,
} from '@angular/core/testing';

import {TestComponentBuilder} from '@angular/compiler/testing';

import {Component, OnChanges, Input} from '@angular/core';

@Component({
    selector: 'test',
    template: `<p>{{value}}</p>`,
})
export class TestComponent implements OnChanges {
    @Input() value: string;

    ngOnChanges(changes: {}): any {
        // should be called
    }
}

fdescribe('TestComponent', () => {
    let tcb: TestComponentBuilder;

    beforeEachProviders(() => [
        TestComponentBuilder,
        TestComponent,
    ]);

    beforeEach(inject([TestComponentBuilder], _tcb => {
        tcb = _tcb;
    }));

    it('should call ngOnChanges', done => {
        tcb.createAsync(TestComponent).then(fixture => {
            let testComponent: TestComponent = fixture.componentInstance;

            spyOn(testComponent, 'ngOnChanges').and.callThrough();

            testComponent.value = 'Test';
            fixture.detectChanges();

            expect(testComponent.ngOnChanges).toHaveBeenCalled();
            done();
        }).catch(e => done.fail(e));
    });
});

Unfortunately the test fails with the message Expected spy ngOnChanges to have been called. I know that I could just check the contents of the HTML Element in this example, but I have some code that needs to be tested inside of the ngOnChanes lifecycle hook, so thats not a solution for me. I also don't want to call testComponent.ngOnChanges({someMockData}); in the test directly.

How can I set the TestComponent.value from a test so that ngOnChanges is called?

like image 492
user1448982 Avatar asked May 24 '16 08:05

user1448982


People also ask

Which of the following is correct about lifecycle hook ngOnChanges?

Q 5 - Which of the following is correct about lifecycle hook - ngOnChanges. A - When the value of a data bound property changes, then this method is called. B - This is called whenever the initialization of the directive/component after Angular first displays the data- bound properties happens.

Which is not a hook in angular 2 application life cycle?

Which of the following is not a hook in Angular 2 application life cycle. ngViewStart is not a hook in Angular 2 application life cycle.

What is the difference between ngOnInit and ngOnChanges?

ngOnInit() is used to execute any piece of code for only one time (for eg : data fetch on load). ngOnChanges() will execute on every @Input() property change. If you want to execute any component method, based on the @Input() value change, then you should write such logic inside ngOnChanges() .

Is ngOnChanges called before ngOnInit?

ngOnChanges( ) — It is called before ngOnInit( ) and whenever one or more data-bound input properties change. It detects simple changes in the property values. ngOnInit( ) — It initializes the directive/component after Angular displays the data-bound properties and is called once after ngOnChanges( ).


3 Answers

Guess I'm a little late to the party, However this may be useful to some one in the future.

There have been a few changes to testing since RC 5 of angular has been released. However the main issue over here is ngOnChanges is not called when inputs are set programmatically. See this for more info . Basically the OnChanges hook is triggered when inputs are passed via the view only.

The solution to this would be to have host component which would be the parent of the test component and pass inputs to through the host component's template.

Here is the complete working code :

import {Component, OnChanges, Input, ViewChild} from '@angular/core';
import { TestBed }      from '@angular/core/testing';

@Component({
    selector: 'test',
    template: `<p>{{value}}</p>`,
})
export class TestComponent implements OnChanges {
    @Input() value: string;

    ngOnChanges(changes: {}): any {
        // should be called
    }
}
/* In the host component's template we will pass the inputs to the actual
 * component to test, that is TestComponent in this case
 */
@Component({
    selector : `test-host-component`,
    template :
    `<div><test [value]="valueFromHost"></test></div>`
})
export class TestHostComponent {
    @ViewChild(TestComponent) /* using viewChild we get access to the TestComponent which is a child of TestHostComponent */
    public testComponent: any;
    public valueFromHost: string; /* this is the variable which is passed as input to the TestComponent */
}

describe('TestComponent', () => {

    beforeEach(() => {
        TestBed.configureTestingModule({declarations: [TestComponent,TestHostComponent]}); /* We declare both the components as part of the testing module */
    });

    it('should call ngOnChanges', ()=> {
        const fixture = TestBed.createComponent(TestHostComponent);
        const hostComponent = fixture.componentInstance;
        hostComponent.valueFromHost = 'Test';
        const component = hostComponent.testComponent;
        spyOn(component, 'ngOnChanges').and.callThrough();
        fixture.detectChanges();
        expect(component.ngOnChanges).toHaveBeenCalled();
    })


});
like image 87
Kiran Yallabandi Avatar answered Oct 18 '22 04:10

Kiran Yallabandi


You also have an option to call ngOnChanges hook manually and pass desired changes object there. But this doesn't set the component properties, only call change logic.

const previousValue = moment('2016-03-01T01:00:00Z');
const currentValue = moment('2016-02-28T01:00:00Z');

const changesObj: SimpleChanges = {
  prop1: new SimpleChange(previousValue, currentValue),
};

component.ngOnChanges(changesObj);

Be aware that this approach will work fine to test the logic inside ngOnChanges, but it will not test that the @Input decorators are properly set.

like image 33
s-f Avatar answered Oct 18 '22 05:10

s-f


In Angular 4, to manually trigger ngOnChanges() when testing, you'll have to manually make the call (as pointed out above), only you need to match the new call signature of SimpleChange():

let prev_value = 'old';
let new_value = 'new';
let is_first_change: boolean = false;

component.ngOnChanges({
  prop1: new SimpleChange(prev_value, new_value, is_first_change),
});
like image 26
The Aelfinn Avatar answered Oct 18 '22 05:10

The Aelfinn