I have a component. Inside of it, the ngOnInit function calls another function of component to retrieve user List. I want to make two series of tets:
The first test, with ngOnInit trigger, when I call fixture.detectChanges() works properly.
My problem is when testing the refresh function: as soon as I call fixture.detectChanges(), ngOnInit is triggered and then I am unable to know where my results come from and if my refresh() function will be tested properly.
Is there any way, before my second series of tests on refresh()
method, to "delete" or "block" the ngOnInit()
so it's not called on fixture.detectChanges()
?
I tried to look at overrideComponent
but it seems it doesn't allow to delete ngOnInit()
.
Or is there any way to detect changes other than using fixture.detectChanges
in my case?
Here is the code for component, stub service and my spec files.
import { Component, OnInit, ViewContainerRef } from '@angular/core';
import { UserManagementService } from '../../shared/services/global.api';
import { UserListItemComponent } from './user-list-item.component';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html'
})
export class UserListComponent implements OnInit {
public userList = [];
constructor(
private _userManagementService: UserManagementService,
) { }
ngOnInit() {
this.getUserList();
}
onRefreshUserList() {
this.getUserList();
}
getUserList(notifyWhenComplete = false) {
this._userManagementService.getListUsers().subscribe(
result => {
this.userList = result.objects;
},
error => {
console.error(error);
},
() => {
if (notifyWhenComplete) {
console.info('Notification');
}
}
);
}
}
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
async,
fakeAsync,
ComponentFixture,
TestBed,
tick,
inject
} from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
// Components
import { UserListComponent } from './user-list.component';
// Services
import { UserManagementService } from '../../shared/services/global.api';
import { UserManagementServiceStub } from '../../testing/services/global.api.stub';
let comp: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let service: UserManagementService;
describe('UserListComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [UserListComponent],
imports: [],
providers: [
{
provide: UserManagementService,
useClass: UserManagementServiceStub
}
],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents();
}));
tests();
});
function tests() {
beforeEach(() => {
fixture = TestBed.createComponent(UserListComponent);
comp = fixture.componentInstance;
service = TestBed.get(UserManagementService);
});
it(`should be initialized`, () => {
expect(fixture).toBeDefined();
expect(comp).toBeDefined();
});
it(`should NOT have any user in list before ngOnInit`, () => {
expect(comp.userList.length).toBe(0, 'user list is empty before init');
});
it(`should get the user List after ngOnInit`, async(() => {
fixture.detectChanges(); // This triggers the ngOnInit and thus the getUserList() method
// Works perfectly. ngOnInit was triggered and my list is OK
expect(comp.userList.length).toBe(3, 'user list exists after init');
}));
it(`should get the user List via refresh function`, fakeAsync(() => {
comp.onRefreshUserList(); // Can be commented, the test will pass because of ngOnInit trigger
tick();
// This triggers the ngOnInit which ALSO call getUserList()
// so my result can come from getUserList() method called from both source: onRefreshUserList() AND through ngOnInit().
fixture.detectChanges();
// If I comment the first line, the expectation is met because ngOnInit was triggered!
expect(comp.userList.length).toBe(3, 'user list after function call');
}));
}
import { Observable } from 'rxjs/Observable';
export class UserManagementServiceStub {
getListUsers() {
return Observable.from([
{
count: 3,
objects:
[
{
id: "7f5a6610-f59b-4cd7-b649-1ea3cf72347f",
name: "user 1",
group: "any"
},
{
id: "d6f54c29-810e-43d8-8083-0712d1c412a3",
name: "user 2",
group: "any"
},
{
id: "2874f506-009a-4af8-8ca5-f6e6ba1824cb",
name: "user 3",
group: "any"
}
]
}
]);
}
}
I tried some "workaround" but I found it to be a little.... verbose and maybe overkill!
For example:
it(`should get the user List via refresh function`, fakeAsync(() => {
expect(comp.userList.length).toBe(0, 'user list must be empty');
// Here ngOnInit is called, so I override the result from onInit
fixture.detectChanges();
expect(comp.userList.length).toBe(3, 'ngOnInit');
comp.userList = [];
fixture.detectChanges();
expect(comp.userList.length).toBe(0, 'ngOnInit');
// Then call the refresh function
comp.onRefreshUserList(true);
tick();
fixture.detectChanges();
expect(comp.userList.length).toBe(3, 'user list after function call');
}));
Preventing lifecycle hook (ngOnInit
) from being called is a wrong direction. The problem has two possible causes. Either the test isn't isolated enough, or testing strategy is wrong.
Angular guide is quite specific and opinionated on test isolation:
However, it's often more productive to explore the inner logic of application classes with isolated unit tests that don't depend upon Angular. Such tests are often smaller and easier to read, write, and maintain.
So isolated tests just should instantiate a class and test its methods
userManagementService = new UserManagementServiceStub;
comp = new UserListComponent(userManagementService);
spyOn(comp, 'getUserList');
...
comp.ngOnInit();
expect(comp.getUserList).toHaveBeenCalled();
...
comp.onRefreshUserList();
expect(comp.getUserList).toHaveBeenCalled();
Isolated tests have a shortcoming - they don't test DI, while TestBed tests do. Depending on the point of view and testing strategy, isolated tests can be considered unit tests, and TestBed tests can be considered functional tests. And a good test suite can contain both.
In the code above should get the user List via refresh function
test is obviously a functional test, it treats component instance as a blackbox.
A couple of TestBed unit tests can be added to fill the gap, they probably will be solid enough to not bother with isolated tests (although the latter are surely more precise):
spyOn(comp, 'getUserList');
comp.onRefreshUserList();
expect(comp.getUserList).toHaveBeenCalledTimes(1);
...
spyOn(comp, 'getUserList');
spyOn(comp, 'ngOnInit').and.callThrough();
tick();
fixture.detectChanges();
expect(comp.ngOnInit).toHaveBeenCalled();
expect(comp.getUserList).toHaveBeenCalledTimes(1);