|
Publié le par Chloé MAHALIN

Angular 4

Tester un composant Angular

Le test unitaire avec Angular4

Le test unitaire avec Angular4, ne nous le cachons pas, c'est un peu douloureux. Lorsque l'on connaît les performances et les capacités de Karma, on ne peut que constater que le côté 'unitaire' des tests est un peu mis de côté au profit de gigantesques mocks.

Étant donné qu'il n'est pas possible d'instancier unitairement un composant, nous nous retrouvons dans l'obligation de mocker un contexte angular complet (On se croirait un peu dans du test de conf Spring sur les bords).

Mais le test, c'est important, et puisqu'une solution existe alors restons sur le principe que c'est mieux que rien ! C'est parti !

Le concept du test de composant

il faut bien comprendre le concept derrière les tests unitaires proposés par le framework Angular4. Tout d'abord, la pierre angulaire de votre application, pour rappel, c'est votre fichier app.module.ts.

Ce fichier, c'est un peu la bibliothèque centrale de votre application. Vous y déclarez vos composants et l'endroit ou ils se trouvent, les librairies externes dont vous aurez besoin, les URLS internes à votre application... Bref, c'est le coeur de votre application. Votre composant n'est pas déclaré au sein de votre NgModule ? Alors il n'existe pas. Vous avez la dépendance de votre librairie externe mais n'avez pas ajouté sa déclaration dans le NgModule ? Cette librairie externe n'est pas dans votre projet.

Et pour les tests ?

Et bien, pour les tests... On en a aussi besoin, de ce noyau central ! Et là... C'est un peu là ou le bas blesse, parce que vous allez devoir utiliser le mock fourni par Angular et déclarer les éléments dont votre composant a besoin pour fonctionner.

Exemple ! Prenons un composant simple :

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-component',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  title: string = 'toto';

}

app.component.html

<h1>{{ title }}</h1>

Pour tester le remplacement du titre dans une balise de type h1, je vais devoir rédiger le test suivant (explications plus bas) :

app.component.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';

import { AppComponent } from '../../../app/app.component';

describe('Test Component :', () => {

  let comp:                             AppComponent;
  let fixture:                          ComponentFixture<AppComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ AppComponent ] // declare the test component
    });

    fixture = TestBed.createComponent(AppComponent); // AppComponent View test instance
    comp = fixture.componentInstance;                // AppComponent Model test instance
  });

  it('h1 tag should have been filled with component title variable', () => {
    fixture.detectChanges();
    const el = fixture.debugElement.query(By.css('h1')).nativeElement;
    expect(el.textContent).toContain(comp.title);
    expect(el.textContent).toEqual('toto');
  });
});

Si la syntaxe des tests vous perturbe, je vous invite à lire l'article sur les tests avec Jasmine.

Voici le minimum pour tester le composant plus haut. Et ce composant ne contenait rien de particulier. Juste un titre.

Plus de détails !

Le TestBed, c'est le mock du core Angular. Et le mock de votre noyau app.module.ts, c'est le bout de code suivant :

TestBed.configureTestingModule({
  declarations: [ AppComponent ]
});

Dans notre cas, il est petit, mais sa taille peut varier si vous ne faites pas assez attention ! En passant, je vous conseille le lire l'article sur le mock de composants enfant dans les tests.

Il fonctionne comme votre @NgModule dans votre fichier app.module.ts. Vous disposez des champs :

TestBed.configureTestingModule({
    imports: [ HttpModule ],
    declarations: [ AppComponent ],
    providers:    [ AppService ]
});

Si vous devez tester un formulaire qui lance une requête de création à un de vos services, alors votre fichier de test ressemblera à ceci :

app.formComponent.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder, FormsModule, ReactiveFormsModule  } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';

import { ApplicationService } from '../../../app/services/applicationService';
import { FormComponent } from '../../../app/components/app.formComponent';

describe('Creation Form Component Test :', () => {

  let comp:               FormComponent;
  let fixture:            ComponentFixture<ApplicationForm>;
  let applicationService: ApplicationService;
  let spy:                jasmine.Spy;

  beforeEach(() => {

    TestBed.configureTestingModule({
      imports: [ FormsModule, ReactiveFormsModule, HttpModule ],
      declarations: [ FormComponent ],
      providers:    [ ApplicationService, FormBuilder ]
    });

    fixture = TestBed.createComponent(FormComponent);
    comp = fixture.componentInstance;

    // Get the applicationService actually injected into the component
    applicationService = fixture.debugElement.injector.get(ApplicationService);
  });

});

Ici, on constate que l'on a du indiquer à notre mock de NgModule, les modules dont on aurait besoin pour nos tests unitaires :

TestBed.configureTestingModule({
  imports: [ FormsModule, ReactiveFormsModule, HttpModule ],
  declarations: [ FormComponent ],
  providers:    [ ApplicationService, FormBuilder ]
});

Nous devons déclarer les modules de formulaire dynamique et les modules HTTP et gérer les classes injectées (providers) à passer à notre composant de test.

Le test du modèle

Notre classe de test a instancié deux variables : comp, qui représente le modèle, et fixture, qui représente la vue.

app.component.spec.ts

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ AppComponent ] // declare the test component
    });

    fixture = TestBed.createComponent(AppComponent); // AppComponent View test instance
    comp = fixture.componentInstance;                // AppComponent Model test instance
  });

Pour tester les méthodes de votre modèle, le componentInstance sera votre meilleur allié. C'est même assez facile avec Jasmine. Rajoutons une méthode à notre composant !

app.component.ts

import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-component',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  title: string = 'toto';
  @Output() onUpdate: EventEmitter<string> = new EventEmitter<string>();

  updateView() {
    this.title = 'tata';
    this.onUpdate.emit(this.title);
  }
}

Pour explication, sur un appel à la méthode de mise à jour, le titre va changer et un événement de mise à jour sera émis. Si un autre composant est abonné à cet événement, il lancera une méthode qu'il a défini.

Testons le changement de titre et l'émission de l'événement :

app.component.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';

import { AppComponent } from '../../../app/app.component';

describe('Test Component :', () => {

  let comp:                             AppComponent;
  let fixture:                          ComponentFixture<AppComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ AppComponent ] // declare the test component
    });

    fixture = TestBed.createComponent(AppComponent); // AppComponent View test instance
    comp = fixture.componentInstance;                // AppComponent Model test instance
  });

  it('h1 tag should have change when update method is called', () => {
    comp.updateView();
    fixture.detectChanges();
    const el = fixture.debugElement.query(By.css('h1')).nativeElement;

    expect(el.textContent).toContain(comp.title);   // Check that the content of the view is still the same as the model.
    expect(el.textContent).toEqual('tata');         // Check that the content of the view was updated.
  });

  it('an update event should have been emitted when update method is called', () => {
    spyOn(comp.onUpdate, 'emit');  // Spy on the emitter 'emit' method.

    comp.updateView();

    expect(comp.onUpdate.emit).toHaveBeenCalledWith('tata');  // Check that the emit method was called with the right value given.
  });
});

J'ai préféré faire deux méthodes pour expliciter le sens de ce qui est testé. A vous de voir la finesse de vos tests unitaires !

Le test de la vue

Tester la vue, c'est un peu plus laborieux, je trouve. Surtout lorsque l'on cherche à tester certains champs de formulaires. Prenons un exemple simple, nous ferons des articles spécifiques pour ces champs.

Allez, on garde le composant de la partie précédente, et on rajoute un bouton qui appelle la méthode :

app.component.ts

import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-component',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  title: string = 'toto';
  @Output() onUpdate: EventEmitter<string> = new EventEmitter<string>();

  updateView() {
    this.title = 'tata';
    this.onUpdate.emit(this.title);
  }
}

app.component.html

<h1>{{ title }}</h1>

<button (click)="updateView();">Validate</button>

Et voici le test :

app.component.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';

import { AppComponent } from '../../../app/app.component';

describe('Test Component :', () => {

  let comp:                             AppComponent;
  let fixture:                          ComponentFixture<AppComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ AppComponent ] // declare the test component
    });

    fixture = TestBed.createComponent(AppComponent); // AppComponent View test instance
    comp = fixture.componentInstance;                // AppComponent Model test instance
  });

  it('click on validate button should trigger the update method', () => {
    spyOn(comp, 'updateView');  // Spy on the emitter 'emit' method.

    // trigger the click
    const button = fixture.debugElement.query(By.css('button')).nativeElement;
    button.dispatchEvent(new Event('click'));

    expect(comp.updateView).toHaveBeenCalled();
  });

});

Nous venons de tester que la vue déclenche bien l'appel vers la méthode lors du clic !

Bref, vous comprenez maintenant la mécanique derrière le test unitaire avec Angular4 !