web-dev-qa-db-fra.com

Angular tester comment empêcher l'appel de ngOnInit pour tester directement une méthode

Le contexte

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:

  • Premier test, le ngOnInit est correctement déclenché et remplit la liste des utilisateurs
  • Dans un second temps, je veux tester ma fonction d'actualisation qui appelle également getUserList ()

Le premier test, avec le déclencheur ngOnInit, lorsque j'appelle fixture.detectChanges () fonctionne correctement.

Problème

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?

Code

Voici le code pour composant, service stub et mes fichiers de spécifications.

Composant

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

Fichier de spécification de composant

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

Service de remplacement (si nécessaire)

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"
          }
        ]
      }
    ]);
  }
}

Mes épreuves

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');
}));
19
BlackHoleGalaxy

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);
22
Estus Flask
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');
}));

Exemple de Plunker

9
yurzui