I developed a project where I implemented a Component that fetches data asynchronously from a service and displays it to the user.
The Component code is as follows:
@Component({
changeDetection: ChangeDetectionStrategy.Default,
selector: 'test-component',
template: `<div id="my-special-property">{{ myProperty }}</div>`,
})
export class DashboardComponent implements OnInit {
private readonly someService = inject(SomeService);
myProperty: string;
ngOnInit(): void {
this.someService.getSomeDataAsync().subscribe((value) => {
this.myProperty = value;
});
}
}
A simple test is set up for this Component:
it('should correctly display myProperty in the HTML template of the Component', () => {
const myMockedValue = 'Some mock value;'
const { fixture, component, page } = setup();
const someService = TestBed.inject(SomeService);
spyOn(someService, 'getSomeDataAsync').and.returnValue(myMockedValue);
fixture.detectChanges();
expect(page.getMySpecialPropertyLocator().innerHTML).toContain(myMockedValue);
});
Everything was functioning perfectly until I decided to change the ChangeDetectionStrategy
to OnPush
. The Component broke because the change detection wasn't triggering after updating the value of myProperty
:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'test-component',
template: `<div id="my-special-property">{{ myProperty }}</div>`,
})
export class DashboardComponent implements OnInit {
private readonly someService = inject(SomeService);
myProperty: string;
ngOnInit(): void {
this.someService.getSomeDataAsync().subscribe((value) => {
this.myProperty = value;
});
}
}
Interestingly, the test still passed, as the change detection was triggered by the fixture.detectChanges()
.
This inconsistency meant that the test was giving false positives. To address this issue, manual change detection needed to be added in the Component:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'test-component',
template: `<div id="my-special-property">{{ myProperty }}</div>`,
})
export class DashboardComponent implements OnInit {
private readonly someService = inject(SomeService);
private readonly changeDetectorRef = inject(ChangeDetectorRef);
myProperty: string;
ngOnInit(): void {
this.someService.getSomeDataAsync().subscribe((value) => {
this.myProperty = value;
this.changeDetectorRef.detectChanges(); // this fixes the problem
});
}
}
If the fixture.detectChanges()
was removed from the test in an attempt to reflect real behavior, the test would fail:
it('should correctly display myProperty in the HTML template of the Component', () => {
const myMockedValue = 'Some mock value;'
const { fixture, component, page } = setup();
const someService = TestBed.inject(SomeService);
spyOn(someService, 'getSomeDataAsync').and.returnValue(myMockedValue);
// This will fail
expect(page.getMySpecialPropertyLocator().innerHTML).toContain(myMockedValue);
});
The goal was to have a realistic test that only passes when manual change detection is applied, avoiding the need to call component.ngOnInit()
within the test manually:
it('should correctly display myProperty in the HTML template of the Component', () => {
const myMockedValue = 'Some mock value;'
const { fixture, component, page } = setup();
const someService = TestBed.inject(SomeService);
spyOn(someService, 'getSomeDataAsync').and.returnValue(myMockedValue);
component.ngOnInit();
expect(page.getMySpecialPropertyLocator().innerHTML).toContain(myMockedValue);
});
What would be the best approach to achieve this without needing to manually call component.ngOnInit()
in the unit test?
In essence, how can we ensure that our unit test accurately reflects the behavior of the Component in a real application scenario with OnPush
change detection?
Please note that this example is simplified. The actual Component can be found here:
https://github.com/azerothcore/Keira3/tree/master/src/app/features/dashboard