|
Publié le par Chloé MAHALIN

Angular 4

Tester un composant Angular

Mocker un composant enfant dans son parent avec Angular

Mocker un composant enfant dans un composant parent est extrèmement sensible. Il existe beaucoup de manières de faire et toutes ne sont pas respectueuse de la quantité d'éléments à instancier pour votre jeu de tests unitaires.

Voici la solution que je propose et qui respecte l'injection de composants (@ViewChild ou @ViewChildren).

Voici les deux composants qui s'imbriquent :

Le composant parent

app.component.ts

import { Component } from '@angular/core';
import { ChildComponent } from './child.component';

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

  @ViewChild (ChildComponent) childComponent: ChildComponent;

  updateView() {
    this.childComponent.setTitle('tata');
  }
}

app.component.html

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

Le composant enfant

child.component.ts

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

@Component({
  selector: 'child-component',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {
  title: string = 'toto';
  setTitle(newTitle: string): void { this.title = newTitle };
}

child.component.html

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

Préparation du test du parent vers l'enfant

Dans un premier temps, je vais devoir modifier le parent, car il est trop rigide dans son implémentation pour accepter de remplacer les instances définies par les @ViewChild par des mocks :

app.component.ts

import { Component } from '@angular/core';
import { ChildComponent } from './child.component';

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

  @ViewChild ('childComponent') childComponent: ChildComponent;     <== ici je définie un id propre au composant

  updateView(): void {
    this.childComponent.setTitle('tata');
  }
}

app.component.html

<child-component #childComponent></child-component>                  <== L'id dans le model doit correspondre à l'ID de la vue.
<button (click)="updateView();">Validate</button>

Pour notre application, cela ne change rien, mais cela va nous permettre de tester plus facilement notre comportement !

Définissons la structure de notre fichier de test :

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

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

// This is the mock of our child component !
@Component({
  selector: 'child-component',
  templateUrl: './app.component.html'
})
class MockChildComponent {
  updateView(): void {}
}

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

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

  beforeEach(() => {

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

    fixture = TestBed.createComponent(AppComponent);
    fixture.autoDetectChanges();
    comp = fixture.componentInstance;
  });

});

Nous avons créé un mock dans notre classe de test, qui a le même selector que notre composant que l'on souhaite tester. Nous lui avons créé une méthode identique à ChildComponent, afin que le mapping se fasse sans heurt. Nous avons déclaré à notre NgModule de test les composants qui allaient intervenir au court de ce test : AppComponent et MockChildComponent dans le rôle de ChildComponent. Nous sommes maintenant prêts à tester.

Test du comportement !

Voici ce que nous allons tester : si je clique sur le bouton du parent, je veux constater l'appel à la méthode d'update de l'enfant !

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

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

// This is the mock of our child component !
@Component({
  selector: 'child-component',
  templateUrl: './app.component.html'
})
class MockChildComponent {
  updateView(): void {}
}

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

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

  beforeEach(() => {

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

    fixture = TestBed.createComponent(AppComponent);
    fixture.autoDetectChanges();
    comp = fixture.componentInstance;
  });

  it('click on validate button should call for update of child', () => {
    spyOn(comp.childComponent, 'updateView');

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

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

});

Voici une manière simple et peu coûteuse de tester la communication entre deux composants !