End to End
Unit testing
Integration and functional testing
Permet d'isoler le code que l'on veut tester.
Types de mocks:
Types de tests unitaires dans Angular
Autres
Exemple de test de base:
describe('my first test', () => {
  let sut; // system under test
  beforeEach(() => {
    sut = {};
  
  });
  it('should be true if true', () => {
    // arrange
    sut.a = false;
    
    // act
    sut.a = true;
    
    //assert
    expect(true).toBe(true);
  });
});
Par défaut, avec un projet Angular:
$ npm test
Structure:
DAMP vs DRY:
Tell the story:
it()beforeEach()it()it()Exemple de pipe:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
  name: 'strength'
})
export class StrengthPipe implements PipeTransform {
  transform(value: number): string {
    if(value < 10) {
      return value + " (weak)";
    } else if(value >= 10 && value < 20) {
      return value + " (strong)";
    } else {
      return value + " (unbelievable)";
    }
  }
}
Le test:
import { StrengthPipe } from "./strength.pipe";
describe('StrengthPipe', () => {
  it('should display weak if strength is 5', () => {
    let pipe = new StrengthPipe();
    expect(pipe.transform(5)).toEqual('5 (weak)');
  })
  it('should display strong if strength is 10', () => {
    let pipe = new StrengthPipe();
    expect(pipe.transform(10)).toEqual('10 (strong)');
  })
})
import { Injectable } from '@angular/core';
@Injectable()
export class MessageService {
  messages: string[] = [];
  add(message: string) {
    this.messages.push(message);
  }
  clear() {
    this.messages = [];
  }
}
import { MessageService } from "./message.service"; describe('MessageService', () => { let service: MessageService; beforeEach(() => { }); it('should have no messages to start', () => { service = new MessageService(); expect(service.messages.length).toBe(0); }); it('should add a message when add is called', () => { service = new MessageService(); service.add('message1'); expect(service.messages.length).toBe(1); }); it('should remove all messages when clear is called', () => { service = new MessageService(); service.add('message1'); service.clear(); expect(service.messages.length).toBe(0); }); });
import { Component, OnInit } from '@angular/core'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) export class HeroesComponent implements OnInit { heroes: Hero[]; constructor(private heroService: HeroService) { } ngOnInit() { this.getHeroes(); } getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } add(name: string): void { name = name.trim(); var strength = 11 if (!name) { return; } this.heroService.addHero({ name, strength } as Hero) .subscribe(hero => { this.heroes.push(hero); }); } delete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); } }
import { HeroesComponent } from "./heroes.component"; import { of } from "rxjs"; describe('HeroesComponent', () => { let component: HeroesComponent; let HEROES; let mockHeroService; beforeEach(() => { HEROES = [ {id:1, name: 'SpiderDude', strength: 8}, {id:2, name: 'Wonderful Woman', strength: 24}, {id:3, name: 'SuperDude', strength: 55} ] mockHeroService = jasmine.createSpyObj(['getHeroes', 'addHero', 'deleteHero']) component = new HeroesComponent(mockHeroService); }) describe('delete', () => { it('should remove the indicated hero from the heroes list', () => { mockHeroService.deleteHero.and.returnValue(of(true)) component.heroes = HEROES; component.delete(HEROES[2]); expect(component.heroes.length).toBe(2); }) it('should call deleteHero', () => { mockHeroService.deleteHero.and.returnValue(of(true)) component.heroes = HEROES; component.delete(HEROES[2]); expect(mockHeroService.deleteHero).toHaveBeenCalledWith(HEROES[2]); }) }) })
Dans le component, si on a une fonction tel que:
delete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); }
La troisième ligne, avec le subscribe(), n'est pas nécessairement testée si on appelle delete et qu'on valide que this.heroes a changé.  C'est dû au fait que le subscribe() ne change pas l'état du component.
Donc on peut faire un test d'intégration avec une vérification d'appel:
it('should call deleteHero', () => {
  mockHeroService.deleteHero.and.returnValue(of(true))
  component.heroes = HEROES;
  component.delete(HEROES[2]);
  expect(mockHeroService.deleteHero).toHaveBeenCalledWith(HEROES[2]);
});
La mention NO_ERRORS_SCHEMA permet d'isoler le composant sous test en faisant en sorte que les sous-composants n'ont pas besoin d'être connus.
import {NO_ERRORS_SCHEMA} from '@angular/core';
TestBed.configureTestingModule({
  declarations: [WriteUsDialogComponent],
  imports: [],
  providers: [],
  schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
L'effet pervers de ne pas avoir d'erreurs quand un sous-composant est _inconnu_, c'est que si on écrit <buttons></buttons>, il n'y aura pas de problème de relevé alors qu'il y en a véritablement un.
component.client = {firstName: 'John', lastName: 'Doe'};
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('a').textContent).toContain('John');
DebugElement est un wrapper sur NativeElement qui a plusieurs fonctionnalités semblable au NativeElement.
expect(fixture.debugElement.query(By.css('a')).nativeElement.textContent).toContain('John');
const de = fixture.debugElement.query(By.css('a')); // de = debugElement
expect(de.nativeElement.textContent).toContain('John');
DebugElement permet d'avoir accès aux directives, par exemple routerLink.
Pour les tests de component qui ont besoin de services, on peut utiliser le TestBed.
describe('HeroComponent (shallow tests)', () => { let fixture: ComponentFixture<HeroComponent>; // le fixture est un wrapper sur le component à tester let mockHeroService; beforeEach(() => { HEROES = [ {id:1, name: 'SpiderDude', strength: 8}, {id:2, name: 'Wonderful Woman', strength: 24}, {id:3, name: 'SuperDude', strength: 55} ]; mockHeroService = jasmine.createSpyObj(['getHeroes', 'addHero', 'deleteHero']); TestBed.configureTestingModule({ declarations: [HeroComponent], providers: [{provide: HeroService, useValue: mockHeroService}], schemas: [NO_ERRORS_SCHEMA], }); fixture = TestBed.createComponent(HeroComponent); }); it('should have the correct hero', () => { fixture.componentInstance.hero = {id: 1, name: 'SuperDude', strenght: 3}; expect(fixture,componentInstance. }); it('should set heroes correctly from the service', () => { mockHeroService.getHeroes.and.returnValue(of(HEROES)) fixture.detectChanges(); expect(fixture.componentInstance.heroes.length).toBe(3); }); });
Pour info, getHeroes() appelle getHeroes() du service qui est un observable:
getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); }
Créer un mock de component:
@Component({ selector: 'app-hero', template: '<div></div>', }) class FakeHeroComponent { @Input() hero: Hero; // Pas obligé de typer ici vu que c'est un mock }
Ajouter FakeHeroComponent dans declarations et enlever le NO_ERRORS_SCHEMA du TestBed.
TestBed.configureTestingModule({ declarations: [ HeroesComponent, FakeHeroComponent ], providers: [ { provide: HeroService, useValue: mockHeroService } ], });
it('should create one li element for each hero', () => { mockHeroService.getHeroes.and.returnValue(of(HEROES)) fixture.detectChanges(); expect(fixture.debugElement.queryAll(By.css('li')).length).toBe(3); });
Dans ces tests, nous voulons évaluer l'intégration entre deux components parent-enfant.
Le test de base, ressemble à ceci:
describe('HeroesComponent (deep tests)', () => { let fixture: ComponentFixture<HeroesComponent>; let mockHeroService; let HEROES; beforeEach(() => { mockHeroService = jasmine.createSpyObj(['getHeroes', 'addHero', 'deleteHero']); TestBed.configureTestingModule({ declarations: [ HeroesComponent, HeroComponent ], providers: [ { provide: HeroService, useValue: mockHeroService } ], schemas: [NO_ERRORS_SCHEMA] }) fixture = TestBed.createComponent(HeroesComponent); }); it('should be true', () => { expect(true).toBe(true); }); });
On inclus HeroesComponent et HeroComponent.
Le component HeroComponent doit se répéter pour chaque héro trouvé.
Remarquez qu'on utilise By.directive().
it('should render each hero as a HeroComponent', () => { mockHeroService.getHeroes.and.returnValue(of(HEROES)); // run ngOnInit fixture.detectChanges(); const heroComponentDEs = fixture.debugElement.queryAll( By.directive(HeroComponent) ); expect(heroComponentDEs.length).toEqual(3); for (let i = 0; i < heroComponentDEs.length; i++) { expect(heroComponentDEs[i].componentInstance.hero).toEqual(HEROES[i]); } });
On veut tester l'intégration du service HeroService avec le HttpClient.
import { TestBed } from "@angular/core/testing"; import { HeroService } from "./hero.service"; import { MessageService } from "./message.service"; import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; describe('HeroService', () => { let mockMessageService; let httpTestingController: HttpTestingController; let service: HeroService; beforeEach(() => { mockMessageService = jasmine.createSpyObj(['add']); TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ], providers: [ HeroService, {provide: MessageService, useValue: mockMessageService} ] }); httpTestingController = TestBed.get(HttpTestingController); service = TestBed.get(HeroService); }); });
Ce qu'on veut tester, c'est la méthode suivante dans le service:
getHero(id: number): Observable<Hero> { const url = `${this.heroesUrl}/${id}`; return this.http.get<Hero>(url).pipe( tap(_ => this.log(`fetched hero id=${id}`)), catchError(this.handleError<Hero>(`getHero id=${id}`)) ); }
Alors, un exemple de test serait :
it('should call get with the correct URL', () => { service.getHero(4).subscribe(); const req = httpTestingController.expectOne('api/heroes/4'); req.flush({id: 4, name: 'SuperDude', strength: 100}); httpTestingController.verify(); });
Pour obtenir une instance de service, on peut faire:
import { TestBed, inject } from '@angular/core/testing'; // ... it('should call get with the correct URL', inject([HeroService], (service: HeroService) => { // ... }));