Publié le 08/07/2017, rédigé par Chloé MAHALIN

Angular 4

Mocker un service injectable pour tester un composant

Souvent

Pour tester un composant Angular qui dispose d'un service

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

Démonstration du test par mock du service

Maintenant que l'on a défini notre composant et notre 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