|
Publié le par Chloé MAHALIN

Angular 4

Mocker un service injectable pour tester un composant

Souvent, les composants angular se font injecter des services, qui remplissent une fonction comme celle d'envoyer des requêtes auprès d'un serveur distant.

Pour tester un composant Angular qui dispose d'un service, il y a deux solutions. Je vous recommande la seconde, qui est beaucoup plus simple à lire et à gérer.

Définition des composants et services

Mocker un service ressemble énormément au mock d'un composant enfant :

app.component.ts

import { Component, Output, EventEmitter } from '@angular/core';
import { MyService } from '../services/service';

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

  private myService: MyService;
  title: string = 'toto';

  constructor(myService: MyService) {
    this.myService = myService;
  }

  getTitle() {
    myService.getTitleFromId(1).subscribe(
        entity => {this.title = entity; },
        error =>  console.log(<any>error)
    );
  }
}

app.component.html

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

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

service.ts

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

@Injectable()
export class MyService {

  private myUrl = 'api/machin';  // URL to web API
  private http: Http;

  constructor(http: Http) {
    this.http = http;
  }

  getTitleFromId(id: number): Observable<string> {
    const url = `${this.myUrl}/get/${id}`;

    return this.http.get(url)
            .map(res => super.extractSingleEntityFromResponse(res))
            .catch(super.handleError);
  }
  //...
}

La classe service n'est complète, elle est détaillée dans l'article explicatif de la rédaction d'un service effectuant des requetes HTTP.

Démonstration du test par mock du service

Maintenant que l'on a défini notre composant et notre service, voici comment tester le composant en mockant le service :

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement, Component, Injectable } from '@angular/core';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';

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

// This is the mock of our service !
@Injectable
class MockService extends MyService {
  constructor(http: Http) {}
  getTitleFromId(id: number): Observable<string> { return Observable.of('mockedValue'); }
}

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

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

  beforeEach(() => {

    TestBed.configureTestingModule({
      imports : [ HttpModule ],                                             // As your mock extends MyService, it requires to inject http and 
      declarations: [ AppComponent ],                                       // the associated module.
      providers:    [ { provide: MyService, useClass: MockService } ]       //<== You need to use useClass and not use Value.
    });

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

    // service actually injected into the component
    myService = fixture.debugElement.injector.get(MyService);
  });

  it('click on validate button should retrieve the title through service', () => {
    spyOn(myService, 'getTitleFromId');

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

    expect(myService.getTitleFromId).toHaveBeenCalledWith(1);
  });

});

Démonstration de test en épiant et court-circuitant le service

Voici comment tester le composant en interceptant les appels au service :

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

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

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

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

  beforeEach(() => {

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

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

    // service actually injected into the component
    myService = fixture.debugElement.injector.get(MyService);
  });

  it('click on validate button should retrieve the title through service', () => {
    spyOn(myService, 'getTitleFromId');

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

    expect(myService.getTitleFromId).toHaveBeenCalledWith(1);
  });

});

Le contenu est quasiment identique, à la différence que l'on laisse Jasmine intercepter les appels au service avec les Spy. Attention toutefois, si vous n'épiez pas votre service, des requêtes partiront et ralentiront vos tests avec des erreurs 404.