Testing your implementation of ngOnChanges in Angular components

Update 24.05.2018: Example code runs on Angular 5 now.

When you write a custom component in Angular (≥ 2.x) that updates its content whenever input changes you can add all necessary computations to the ngOnChanges lifecycle hook.

@Component({
selector: 'greeter',
inputs: ['name'],
template: `<div>{{ greeting }}</div>`,
})
export class Greeter implements OnChanges {
@Input() name: string;
greeting: string;

constructor() {}

ngOnChanges(changes: SimpleChanges) {
if (changes['name']) {
this.data = 'Hello ' + this.name;
}
}
}

As a good developer you would like to write a unit test for the transformation of your data that happens in ngOnChanges. This is what a first attempt could look like:

describe('Component: Greeter', () => {
let fixture, greeter, element, de;

//setup
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ Greeter ]
});
  fixture = TestBed.createComponent(Greeter);
greeter = fixture.componentInstance;
element = fixture.nativeElement;
});

//specs
it('should render `Hello World!`', () => {
greeter.name = 'World';

fixture.detectChanges();
    expect(element.querySelector('h1').innerText).toBe('Hello World!');
});
})

However, this test will fail, since we manually changed a property of the Greeter component. This means that when we call detectChanges Angular will not detect any changes and not call ngOnChanges. So, how can change our test so ngOnChanges is called?

We have two options. Either we

  • call ngOnChanges ourselves, or
  • wrap Greeter in a host component on which we can change the name property.

Calling ngOnChanges directly

Calling ngOnChanges ourselves follows the view of “isolated unit tests”. By using TestBed we have anyway already diverged from this path so we could also give a try to the second approach. However, let’s quickly look at the first approach, so we can later see the difference to the second one:

it('should render `Hello World!`', () => {
greeter.name = 'World';

//directly call ngOnChanges
greeter.ngOnChanges({
name: new SimpleChange(null, greeter.name)
});
fixture.detectChanges();
  expect(element.querySelector('h1').innerText).toBe('Hello World!');
});

Here, we emulate the way Angular creates the parameters passed on to ngOnChanges.

Wrapping Greeter into a host component

Now let’s look at the second approach. We first define a new component with a property ‘name’ that we can later set during the test:

@Component({
template: '<greeter [name]="name"></greeter>'
})
class TestHostComponent {
name: string
}

During the test we let Angular create the host component instead of Greeter and set the name on the host component. Calling detectChanges will then sync the host component’s name property with its template and thus yield ngOnChanges of Greeter to be called:

describe('Component: Greeter w/ host', () => {
let fixture, testHost, element, de;

//setup
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ Greeter, TestHostComponent ]
});
  fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
element = fixture.nativeElement;
de = fixture.debugElement;
});

//specs
it('should render `Hello World!`', () => {
testHost.name = 'World';

//trigger change detection
fixture.detectChanges();
    expect(element.querySelector('h1').innerText).toBe('Hello World!');
});
})
@Component({
template: '<greeter [name]="name"></greeter>'
})
class TestHostComponent {
name: string
}

You can find the complete code in this Plunkr: https://embed.plnkr.co/hFvWPLXw5ZSPaya6pBLh/