J'ai un composant. À l'intérieur de celui-ci, la fonction ngOnInit appelle une autre fonction de composant pour récupérer la liste d'utilisateurs. Je veux faire deux séries de tets:
Le premier test, avec le déclencheur ngOnInit, lorsque j'appelle fixture.detectChanges () fonctionne correctement.
Mon problème est lors du test de la fonction d'actualisation: dès que j'appelle fixture.detectChanges (), ngOnInit est déclenché et je ne peux pas savoir d'où proviennent mes résultats ni si ma fonction refresh () sera testée correctement.
Y at-il un moyen, avant ma deuxième série de tests sur la méthode refresh()
, de "supprimer" ou de "bloquer" la ngOnInit()
afin qu'elle ne soit pas appelée sur la fonction fixture.detectChanges()
?
J'ai essayé de regarder overrideComponent
mais il semble que cela ne permet pas de supprimer ngOnInit()
.
Ou y a-t-il un autre moyen de détecter les changements que d'utiliser fixture.detectChanges
Dans mon cas?
Voici le code pour composant, service stub et mes fichiers de spécifications.
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"
}
]
}
]);
}
}
J'ai essayé quelques "solutions de contournement" mais j'ai trouvé que c'était un peu ... verbeux et peut-être exagéré!
Par exemple:
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');
}));
Empêcher l'appel du cycle de vie (ngOnInit
) est une mauvaise direction. Le problème a deux causes possibles. Soit le test n'est pas assez isolé, soit la stratégie de test est fausse.
Le guide angulaire est assez spécifique et avisé sur l’isolation du test :
Cependant, il est souvent plus productif d'explorer la logique interne des classes d'applications avec des tests unitaires isolés qui ne dépendent pas de Angular. Ces tests sont souvent plus petits et plus faciles à lire, à écrire et à maintenir.
Des tests si isolés devraient instancier une classe et tester ses méthodes
userManagementService = new UserManagementServiceStub;
comp = new UserListComponent(userManagementService);
spyOn(comp, 'getUserList');
...
comp.ngOnInit();
expect(comp.getUserList).toHaveBeenCalled();
...
comp.onRefreshUserList();
expect(comp.getUserList).toHaveBeenCalled();
Les tests isolés présentent un inconvénient: ils ne testent pas l'ID, contrairement aux tests TestBed. Selon le point de vue et la stratégie de test, des tests isolés peuvent être considérés comme des tests unitaires et des tests TestBed peuvent être considérés comme des tests fonctionnels. Et une bonne suite de tests peut contenir les deux.
Dans le code ci-dessus should get the user List via refresh function
test est évidemment un test fonctionnel, il traite l’instance de composant comme une boîte noire.
Quelques tests unitaires TestBed peuvent être ajoutés pour combler le vide, ils seront probablement suffisamment solides pour ne pas vous soucier des tests isolés (bien que ces derniers soient sûrement plus précis):
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);
it(`should get the user List via refresh function`, fakeAsync(() => {
let ngOnInitFn = UserListComponent.prototype.ngOnInit;
UserListComponent.prototype.ngOnInit = () => {} // override ngOnInit
comp.onRefreshUserList();
tick();
fixture.detectChanges();
UserListComponent.prototype.ngOnInit = ngOnInitFn; // revert ngOnInit
expect(comp.userList.length).toBe(3, 'user list after function call');
}));