Outils pour utilisateurs

Outils du site


web:javascript:angular:unittests

Unit testing in Angular

Testing Overview

End to End

  • Live running application
  • Tests exercise live application
  • Automated
  • Validate application as a hole

Unit testing

  • Single unit of code (often a class)

Integration and functional testing

  • More than a unit, less than the complete application
  • Check that a part is working with another part

Mocking

Permet d'isoler le code que l'on veut tester.

Types de mocks:

  • Dummies
  • Stubs
  • Spies
  • True mocks

Unit Tests in Angular

Types de tests unitaires dans Angular

  • Isolated
  • Integration (création de module)
    • Shallow
    • Deep

Tools of Unit Testing with Angular

  • Karma
  • Jasmin

Autres

  • Jest
  • Mocha/Chai
  • Sinon
  • TestDouble
  • Wallaby
  • Cypress
  • End to end tools

Installing and running

Écrire le premier test unitaire

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);
  });
});

Exécuter les tests unitaires

Par défaut, avec un projet Angular:

$ npm test

Écrire de bons tests unitaires

Structure:

  • Arrange all necessary preconditions and inputs
  • Act on the object or class under test
  • Assert that the expected result have occured

DAMP vs DRY:

  • DRY (don't repeat yourself) → remove duplication
  • DAMP → Repeat yourself if necessary

Tell the story:

  • A test should be a complete story, all within the it()
  • You shouldn't need to look around much to understant the test
  • Techniques
    • Move less interesting setup to beforeEach()
    • Keep critical setup within the it()
    • Include Arrange, Act, and Assert inside the it()

Isolated Unit Tests

Tester un Pipe

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)');
  })
})

Tester un service

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);
  });
 
});

Tester un Component

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]);
    })
  })
})

Interaction test

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]);
});

Shallow Integration Tests

Using NO_ERRORS_SCHEMA

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.

Testing Rendered HTML

component.client = {firstName: 'John', lastName: 'Doe'};

fixture.detectChanges();

expect(fixture.nativeElement.querySelector('a').textContent).toContain('John');

NativeElement vs DebugElement

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.

More complex Shallow Tests

Mocking an injected Service

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);
}

Mocking Child Components

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 }
  ],
});

Dealing with Lists of Elements

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);
});

Deep Integration Tests

Creating a Deep Integration Test

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.

Finding Elements by Directive

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]);
  }
});

Integration Testing of Services

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);
  });
});

Implementing a Test with Mocked HTTP

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();
});

Obtenir l'instance de service

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) => {
  // ...
}));

Testing DOM Interaction and Routing Components

Sources

web/javascript/angular/unittests.txt · Dernière modification : 2022/02/02 00:42 de 127.0.0.1