112 votes

Test unitaire de l'événement "clic" en Angular

J'essaie d'ajouter des tests unitaires à mon application Angular 2. Dans l'un de mes composants, il y a un bouton avec une fonction (click) manipulateur. Lorsque l'utilisateur clique sur le bouton, une fonction est appelée qui est définie dans le fichier .ts fichier de classe. Cette fonction imprime un message dans la fenêtre console.log indiquant que le bouton a été pressé. Mon code de test actuel teste l'impression de la fonction console.log message :

describe('Component: ComponentToBeTested', () => {
    var component: ComponentToBeTested;

    beforeEach(() => {
        component = new ComponentToBeTested();
        spyOn(console, 'log');
    });

    it('should call onEditButtonClick() and print console.log', () => {
        component.onEditButtonClick();
        expect(console.log).toHaveBeenCalledWith('Edit button has been clicked!);
    });
});

Cependant, cela ne teste que la classe du contrôleur, pas le HTML. Je ne veux pas seulement tester que la journalisation se produit quand onEditButtonClick est appelé ; je veux aussi tester que onEditButtonClick est appelé lorsque l'utilisateur clique sur le bouton de modification défini dans le fichier HTML du composant. Comment puis-je le faire ?

155voto

peeskillet Points 32287

Mon objectif est de vérifier si la fonction 'onEditButtonClick' est invoquée lorsque l'utilisateur clique sur le bouton d'édition et non pas de vérifier uniquement l'impression du fichier console.log.

Vous devrez d'abord configurer le test à l'aide de l'outil Angular TestBed . De cette façon, vous pouvez saisir le bouton et le cliquer. Ce que vous allez faire, c'est configurer un module, comme vous le feriez pour un module de type @NgModule juste pour l'environnement de test

import { TestBed, async, ComponentFixture } from '@angular/core/testing';

describe('', () => {
  let fixture: ComponentFixture<TestComponent>;
  let component: TestComponent;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ ],
      declarations: [ TestComponent ],
      providers: [  ]
    }).compileComponents().then(() => {
      fixture = TestBed.createComponent(TestComponent);
      component = fixture.componentInstance;
    });
  }));
});

Alors vous devez espionner le onEditButtonClick cliquez sur le bouton et vérifiez que la méthode a été appelée.

it('should', async(() => {
  spyOn(component, 'onEditButtonClick');

  let button = fixture.debugElement.nativeElement.querySelector('button');
  button.click();

  fixture.whenStable().then(() => {
    expect(component.onEditButtonClick).toHaveBeenCalled();
  });
}));

Ici, nous devons exécuter un async car le clic du bouton contient un traitement asynchrone des événements, et nous devons attendre le traitement de l'événement en appelant fixture.whenStable()

Mise à jour

Il est désormais préférable d'utiliser fakeAsync/tick par opposition à la async/whenStable combo. Cette dernière devrait être utilisée si un appel XHR est effectué, car fakeAsync ne le supporte pas. Ainsi, le code ci-dessus, remanié, ressemblerait à ceci

it('should', fakeAsync(() => {
  spyOn(component, 'onEditButtonClick');

  let button = fixture.debugElement.nativeElement.querySelector('button');
  button.click();
  tick();
  expect(component.onEditButtonClick).toHaveBeenCalled();

}));

N'oubliez pas d'importer fakeAsync et tick .

Voir aussi :

3 votes

Que faire si nous avons plus d'un bouton dans le fichier html ? En faisant quelque chose comme : fixture.debugElement.nativeElement.querySelector('button') ;

2 votes

Cherche 'bouton' dans le fichier html, mais qu'est-ce que nous avons plus de deux boutons ? Comment dois-je référencer l'occurrence du deuxième bouton pour les tests ?

4 votes

J'ai trouvé la solution ! button = fixture.debugElement.queryAll(By.css('button') ; button1 = button[0] ;

58voto

Mav55 Points 1030

Les événements peuvent être testés à l'aide de la fonction async / fakeAsync les fonctions fournies par '@angular/core/testing' En effet, tout événement dans le navigateur est asynchrone et poussé vers la boucle/queue d'événements.

Voici un exemple très basique pour tester l'événement de clic en utilisant fakeAsync .

Le site fakeAsync permet un style de codage linéaire en exécutant le corps du test dans un fichier spécial. fakeAsync zone de test.

Ici, je teste une méthode qui est invoquée par l'événement de clic.

it('should', fakeAsync( () => {
    fixture.detectChanges();
    spyOn(componentInstance, 'method name'); //method attached to the click.
    let btn = fixture.debugElement.query(By.css('button'));
    btn.triggerEventHandler('click', null);
    tick(); // simulates the passage of time until all pending asynchronous activities finish
    fixture.detectChanges();
    expect(componentInstance.methodName).toHaveBeenCalled();
}));

Voici ce que Documentation sur Angular ont à dire :

Le principal avantage de fakeAsync par rapport à async est que le test semble être synchrone. Il n'y a pas de then(...) pour perturber le flux visible du contrôle. Le retour de la promesse fixture.whenStable a disparu, remplacé par tick()

Il y a sont limitations. Par exemple, vous ne pouvez pas effectuer d'appel XHR à partir d'un fichier fakeAsync

0 votes

Y a-t-il une raison de préférer cette solution ou la solution acceptée ? En tant que noob angulaire, je n'ai aucune idée de ce qui est "mieux".

2 votes

@AdamHughes fakeAsync est plus facile à lire et à comprendre selon la documentation angulaire. Mais vous pouvez choisir ce qui vous convient le mieux. Il n'y a rien de tel qu'une solution acceptée lorsqu'on utilise aync ou fakeAsync.

1 votes

J'ai écrit une réponse ici sur la comparaison entre l'utilisation de async vs. fakeAsync . Même si en ma réponse J'ai utilisé async En général, ma préférence va à l'utilisation de fakeAsync .

15voto

renocor Points 884

J'utilise Angulaire 6 . J'ai suivi la réponse de Mav55 et ça a marché. Cependant, je voulais m'assurer que si fixture.detectChanges(); était vraiment nécessaire, alors je l'ai enlevé et ça marche toujours. J'ai ensuite supprimé tick(); pour voir si ça marchait et ça a marché. Finalement, j'ai retiré le test du fakeAsync() et surprise, ça a marché.

Donc j'ai fini avec ça :

it('should call onClick method', () => {
  const onClickMock = spyOn(component, 'onClick');
  fixture.debugElement.query(By.css('button')).triggerEventHandler('click', null);
  expect(onClickMock).toHaveBeenCalled();
});

Et ça a bien marché.

0 votes

Ma stratégie générale a été de toujours invoquer fixture.detectChanges() afin de déclencher les événements du cycle de vie d'Angular. Il se peut que votre composant n'ait pas d'objet de type ngOnInit ou que ce n'était pas nécessaire pour que le test soit réussi.

0 votes

@SnailCoil ou il pourrait utiliser la détection automatique des changements ( angular.io/guide/testing#automatic-change-detection )

1 votes

Certains événements dom sont synchrones. Par exemple, le événement de saisie est documenté pour être synchrone. Si ce que vous expérimentez avec l'événement de clic de bouton est vrai, alors cela signifie que l'événement de clic de bouton est également synchrone. Mais il y a beaucoup d'événements Dom qui sont asynchrone dans ce cas, vous serait besoin d'utiliser async ou fakeAsync . Pour être sûr, je ne vois rien de mal à supposer que tous les événements sont asynchrones et à utiliser simplement async ou fakeAsync (c'est vous qui choisissez).

3voto

Afzal Azad Points 21

Pour vérifier l'événement d'appel du bouton, nous devons d'abord espionner la méthode qui sera appelée après le clic du bouton. donc notre première ligne sera spyOn La méthode d'espionnage prend deux arguments 1) le nom du composant 2) la méthode à espionner, par exemple : 'onSubmit'. N'oubliez pas de ne pas utiliser '()', seul le nom est requis. ensuite nous devons créer l'objet du bouton à cliquer maintenant nous devons déclencher le gestionnaire d'événement sur lequel nous ajouterons l'événement de clic ensuite nous attendons de notre code qu'il appelle la méthode submit une fois

it('should call onSubmit method',() => {
    spyOn(component, 'onSubmit');
    let submitButton: DebugElement = 
    fixture.debugElement.query(By.css('button[type=submit]'));
    fixture.detectChanges();
    submitButton.triggerEventHandler('click',null);
    fixture.detectChanges();
    expect(component.onSubmit).toHaveBeenCalledTimes(1);
});

0 votes

Cela ne semble pas fonctionner pour moi. J'utilise Angular 7. Je pense que la bonne approche est : it('should call onSubmit method',() => { let mock = spyOn(component, 'onSubmit'); let submitButton: DebugElement = fixture.debugElement.query(By.css('button[type=submit]')); fixture.detectChanges(); submitButton.triggerEventHandler('click',null); fixture.detectChanges(); expect(mock).toHaveBeenCalledTimes(1); });

1voto

Casper Points 28

J'ai eu un problème similaire (explication détaillée ci-dessous), et je l'ai résolu (en jasmine-core: 2.52 ) en utilisant le tick avec un nombre de millisecondes identique (ou supérieur) à celui de l'original. setTimeout appeler.

Par exemple, si j'avais un setTimeout(() => {...}, 2500); (pour qu'il se déclenche après 2500 ms), j'appellerais tick(2500) et qui résoudrait le problème.

Ce que j'avais dans mon composant, comme une réaction sur une Supprimer cliquez sur le bouton :

delete() {
    this.myService.delete(this.id)
      .subscribe(
        response => {
          this.message = 'Successfully deleted! Redirecting...';
          setTimeout(() => {
            this.router.navigate(['/home']);
          }, 2500); // I wait for 2.5 seconds before redirect
        });
  }

Elle est mon travail test :

it('should delete the entity', fakeAsync(() => {
    component.id = 1; // preparations..
    component.getEntity(); // this one loads up the entity to my component
    tick(); // make sure that everything that is async is resolved/completed
    expect(myService.getMyThing).toHaveBeenCalledWith(1);
    // more expects here..
    fixture.detectChanges();
    tick();
    fixture.detectChanges();
    const deleteButton = fixture.debugElement.query(By.css('.btn-danger')).nativeElement;
    deleteButton.click(); // I've clicked the button, and now the delete function is called...

    tick(2501); // timeout for redirect is 2500 ms :)  <-- solution

    expect(myService.delete).toHaveBeenCalledWith(1);
    // more expects here..
  }));

P.S. Excellente explication sur fakeAsync et les asynchrones généraux en test peuvent être trouvés ici : une vidéo sur les stratégies de test avec Angular 2 - Julie Ralph, à partir de 8:10, durée 4 minutes :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X