42

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?

Luca Ritossa
  • 1,118
  • 11
  • 22
user1448982
  • 1,200
  • 3
  • 12
  • 22
  • 1
    I don't think you should do test to angular, its on angular team. you just need find any why to test your business codes other than the framework itself. – Jiang YD May 24 '16 at 09:10
  • 4
    I want to test my business code which is inside the `ngOnChanges` function – user1448982 May 24 '16 at 09:53
  • if you need do e2e testing, maybe `http://www.protractortest.org` helps – Jiang YD May 25 '16 at 01:38
  • 2
    This is completely valid scenario. There is an issue about it on [Github](https://github.com/angular/angular/issues/9866) – Dmitry Efimenko Sep 30 '16 at 18:16
  • Being as Angular ignored that github issue for 4 years then a bot automatically closed it without an option to re-open, this isn't getting fixed. – Liam May 15 '20 at 08:19

4 Answers4

75

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


});
Francesco Borzi
  • 56,083
  • 47
  • 179
  • 252
Kiran Yallabandi
  • 2,544
  • 2
  • 22
  • 25
  • 2
    Is there another solution available today? I don't want to write a TestComponent for all my Components... :( – Franziskus Karsunke Mar 10 '17 at 11:37
  • 2
    Well I coudn't find one so far :( . Infact there was an issue raised on this very topic : https://github.com/angular/angular/issues/6235. Read pkozlowski-opensource's comments on this – Kiran Yallabandi Mar 11 '17 at 03:54
  • 7
    if you want specific change : component.ngOnChanges({ 'key': new SimpleChange(false, true) }) – Yoav Schniederman May 09 '17 at 06:31
  • Excellent info, you saved me a bunch of time debugging. thanks – DJ22T Jul 17 '19 at 16:49
  • @YoavSchniederman I wish I could upvote this multiple times. For my testing purposes this is much cleaner than using TestHostComponent –  Oct 02 '19 at 21:22
  • I have some functions in my component along with ngOnChanges(), how do I test the functions??? with test host component I'm only able to access variables not functions – Harish May 13 '20 at 13:49
  • I had to call `fixture.detectChanges();` before `const component = hostComponent.testComponent` – waternova May 29 '20 at 23:00
36

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.

Mario Petrovic
  • 7,500
  • 14
  • 42
  • 62
s-f
  • 2,091
  • 22
  • 28
27

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),
});
Mario Petrovic
  • 7,500
  • 14
  • 42
  • 62
The Aelfinn
  • 13,649
  • 2
  • 54
  • 45
1

We are using this utility function:

import {OnChanges, SimpleChange, SimpleChanges} from '@angular/core';

export function updateComponentInputs<T extends OnChanges>(
    component: T,
    changes: Partial<T>
) {
    const simpleChanges: SimpleChanges = {};

    Object.keys(changes).forEach(changeKey => {
        component[changeKey] = changes[changeKey];
        simpleChanges[changeKey] = new SimpleChange(
            null,
            changes[changeKey],
            false
        );
    });
    component.ngOnChanges(simpleChanges);
}

You would use it like this:

testComponent.updateComponentInputs({value: 'test'});

This sets testComponent.value to 'test', and also calls ngOnChanges with the appropriate change event.

jgosar
  • 2,469
  • 1
  • 16
  • 14