Meng Resumé Blat vun Eenheet Tester am Angular

Fir déi vun iech, déi d'Geleeënheet haten, de Angular Kader ze kucken (V2 a méi), Dir hutt gesinn wéi d'Dokumentatioun op der offizieller Säit voluminös ka sinn, besonnesch op Eenheet Tester Deel. Den Zweck vun dësem Artikel ass Iech e Resuméblat vun Eenheet Testen beschten Praktiken an Angular ze bidden.
Den Artikel gëtt ugeholl datt Dir schonn Angular a seng Eenheetstester benotzt hutt, an datt Dir de lexikalesche Feld vun Eenheetstester kennt.
D'Berodung an d'Léisungen hei ginn aus enger sechs Méint Erfahrung vun der Angular Entwécklung mat engem Team vu fënnef Leit. Si sinn natierlech ënnerleien zu Verbesserung/Diskussiounen.

Preambel: Wéi richteg mat Angular ze "spotzen"?

E klenge Preambel ass néideg fir e Punkt ze klären, dee fir mech ze dacks an der Literatur iwwer dëst Thema iwwersinn ass, an dee fir de Rescht vum Verständnis vital ass:
Spott, Spioun an aner Stubb.
Ze dacks gëtt eng e bësse naiv Approche an Angular Eenheet Testbeispiller ugebueden. Loosst eis huelen de Fall aus dem Test vun enger Komponent am offiziellen Dokument:

class MockUserService {
  isLoggedIn = true;
  user = { name: "Test User" };
}
/* ... */
beforeEach(() => {
  TestBed.configureTestingModule({
    // provide the component-under-test and dependent service
    providers: [
      WelcomeComponent,
      { provide: UserService, useClass: MockUserService }
    ]
  });
  // inject both the component and the dependent service.
  comp = TestBed.get(WelcomeComponent);
  userService = TestBed.get(UserService);
});

Dëse Code erkläert dat fir d'Ëmsetzung vun engem Service ze spotten (hei UserService) erstellt einfach eng falsch Klass déi dëse Service ersetzt (hei MockUserService).
Dës Approche nervt mech grondsätzlech well mir all Typprüfung verléieren. Tatsächlech gëtt et keng Garantie datt MockUserService respektéiert den Interface vun der Klass UserService. Dobäi kommen d'Käschte fir eng falsch Klass ze schreiwen ...
[bctt tweet = "Am Angular, e Spott mat der Hand ze kreéieren ass weder eng sécher nach eng effizient Approche."] Eng aner Manéier fir et ze maachen, deen hei an do gesi ka ginn, ass d'Klass ze verlängeren, déi mir dann wëllen spotten, iwwerlaascht de gewënschte Verhalen. Wa mir op eist viregt Beispill zréckgoen, géif et esou ausgesinn:

class MockUserService extends UserService {
  user = { name: "Another user" };
}
/* ... */
beforeEach(() => {
  TestBed.configureTestingModule({
    // provide the component-under-test and dependent service
    providers: [
      WelcomeComponent,
      FirstLevelNeededService,
      SecondLevelNeededService,
      { provide: UserService, useClass: MockUserService }
    ]
  });
  // inject both the component and the dependent service.
  comp = TestBed.get(WelcomeComponent);
  userService = TestBed.get(UserService);
});

Dës Method ass nach méi schlëmm wéi déi virdru well och wann Dir den Tipp- a Verbositéitsproblem fixéiert hutt, hutt Dir d'Isolatioun gebrach déi néideg ass fir en Eenheetstest.
Wéi déi richteg Ëmsetzung vun UserService importéiert ass, TestBed wäert verlaangen, datt mir et och mat all Klassen op déi UserService. Stellt Iech d'Ofhängegkeetsschema hei ënnen vir:

+----------------------------- --------------------+ | | | | | | | UserService +<---+ FirstLevelNeededService +<----+ SecondLevelNeededService | | | | | | | +----------------------------- --------------------+

No dësem Schema, solle mir déi TestBed, FirstLevelNeededService et SecondLevelNeededService. Dëst mécht kee Sënn wann Dir driwwer denkt well den Zweck vun engem Spott ass d'Behuele vun enger Klass ze simuléieren. Also firwat mock Ofhängegkeeten ubidden?
[bctt tweet = "Erstellt ni e Spott vun enger TypeScript Klass andeems se hir originell Ëmsetzung an Angular verlängeren."] Soss musst Dir eng grouss Unzuel u Klassen fir all Testsuite ubidden.

Benotzt eng Spottbibliothéik

Fir objektorientéiert Entwéckler ass d'Benotzung vun enger Spottbibliothéik offensichtlech am Kontext vun der Eenheetstestung, well hir Sprooch dacks net sou dynamesch ass wéi JavaScript an et net erlaabt Iech Mocks op der Flucht ze schreiwen ("on the fly"). Snatch?" - Wien huet dat gesot?).
Eng Mock-Bibliothéik benotzt d'Typdefinitioun erstallt iwwer TypeScript fir e Spott vun enger Klass ze kreéieren.
Al Reflex vun "Javaiste" vläicht, Ech perséinlech gären ts mockito als Mock säi Bicherhändler, awer et ginn anerer: TS-Mock oder Typmoq
Wa mir eist Beispill mat ts-mockito huelen, géif eise Code esou ausgesinn:

import { instance, mock, when } from "ts-mockito";
/***/
let userServiceMock: UserService;
beforeEach(() => {
  userServiceMock = mock(UserService);
  when(userServiceMock.user).thenReturn({ name: "A user" });
  TestBed.configureTestingModule({
    providers: [
      WelcomeComponent,
      { provide: UserService, useValue: instance(userServiceMock) }
    ]
  });
  userService = TestBed.get(UserService);
});

Einfach, ass et net? ts-mockito erstellt als éischt eng gespotte Klass mat der Funktioun mock erstellt dann mock Instanzen baséiert op dëser Klass mat der Funktioun instance.
Fir Resultater ze simuléieren, benotze mir dacks d'Funktioun when. Ëmmer benotzen when op engem Spott Klass an Opruff instance nëmmen duerno, soss wäert Är mock Instanz net déi definéiert Verhalen hunn.

Am Rescht vum Artikel wäerte mir systematesch ts-mockito benotzen

Guide

Fall n°1: Testen vun engem Service ouni Ofhängegkeet mat Angular

Loosst eis mam einfachsten ufänken, de Service. An Angular ass e Service weder méi nach manner wéi eng TypeScript Klass déi vun der instantiéiert gëtt IOC-Container vun Angular an all aner Elementer injizéiert, déi et als Ofhängegkeeten definéieren.
Wann de Service keng Ofhängegkeet benotzt, déi vu Angular kënnt (wéi de HttpClient zum Beispill) Dir kënnt et testen wéi all TypeScript Klass an all Projet wéi dëst:

import { MyService } from "./my.service";
import { instance, mock, when } from "ts-mockito";
import { MathLib } from "./my.service";
describe("MyService", () => {
  it("should add correctly two positive integers and multiply them by two", () => {
    // given
    const mathMock = mock(MathLib);
    when(mathMock.add(1, 2)).thenReturn(3);
    when(mathMock.multiply(3)).thenReturn(6);
    const myService = new MyService(instance(mathMock));
    // when
    const result = myService.addAndMultiplyByTwo(1, 2);
    // then
    expect(result).toBe(6);
  });
});

Éischte Punkt, Dir wäert feststellen, datt am Géigesaz zu Wénkel-cli mir schafen keen Test should be created. Dësen Test, dee benotzt gëtt fir d'Existenz vun der Klass z'iwwerpréiwen ass de facto scho vun den aneren Eenheetstester verifizéiert, dofir nëtzlos.
Zweeten a potenziell wichtegste Punkt vun dësem Artikel:
[bctt tweet = "Benotzt den 'TestBed' nëmme selten am Angular Eenheet Testen."] TestBed, méi spezifesch den Opruff un seng Method configureTestingModule, ass laang a bréngt méi Komplexitéit an den Tester. A fir gudde Grond, der Method Opruff TestBed.configureTestingModule wäert, wéi säin Numm et scho seet, eng Angularmodul bei all Uruff an instantiéiert all d'Elementer vun dësem 'gefälschte Modul' dank dem IOC Container vun Angular.
Mir hunn an eisen Tester observéiert datt d'Benotzung vun TestBed wäert potenziell fënnef Mol méi laang daueren wéi eng einfach Objektinstantiatioun a seng Spotten wéi an eisem Beispill (méi Info hei): Angular TestBed ass ze laang)
Mir hunn do näischt erfonnt, et heescht a Isoléiert Test am Angular doc (deen ech de Link net méi fannen well en immens ass :D)
Ok, mee wat geschitt wann ech benotzen Module kommen aus Angular wéi de HttpModule ?

Fall n°2: Testen vun engem Service deen vun engem Angular Modul hänkt

Loosst eis e zimlech Standard Test vun engem Authentifikatiounsservice huelen deen den HttpClient :

import { AuthService } from "./auth.service";
import { inject, TestBed } from "@angular/core/testing";
import {
  HttpClientTestingModule,
  HttpTestingController
} from "@angular/common/http/testing";
describe("AuthService", () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [AuthService],
      imports: [HttpClientTestingModule]
    });
  });
  it(
    "should return null user when server responds HTTP error",
    inject(
      [HttpTestingController, AuthService],
      (httpMock: HttpTestingController, service: AuthService) => {
        service.user.subscribe(user => {
          expect(user).toBe(null);
        });
        httpMock.expectOne("auth/user").error(new ErrorEvent("401"));
        httpMock.verify();
      }
    )
  );
});

Mir hu keng aner Wiel wéi den ze benotzen TestBed well et ass hien, deen Zougang zu der erlaabt httpMock responsabel fir Simulatioun Server Äntwerte.
[bctt tweet = "Mir wäerten nëmmen den `TestBed` benotzen fir Komponenten oder Klassen ofhängeg vum Angular Modul ze testen."] Dir wäert och d'Benotzung vum klengen Helfer bemierken inject, praktesch fir Instanzen ze recuperéieren erstallt am Angular's IOC Container. Dir hutt vläicht och gemierkt, datt meng beforeEach enthält keen Opruff un async. Async tendéiert ondiskriminéiert ze benotzen, awer dës Funktioun ass do fir op eng Asynchronie ze waarden.
Wann Dir kuckt op der offiziell Dokumentatioun configureTestingModule net zréck aus Promise, also ass et net asynchron, also ass et net wäert ze ruffen async an dësem Fall.

Fall n°3: Test vun engem "Basis" Komponent

Dëst ass d'Aart vu Komponente déi Dir vill an Angular schreift.
Mir wäerten am folgende Fall gesinn datt d'Komponente, déi héich an der Hierarchie sinn, och d'Thema vun der zousätzlech Veraarbechtung sinn.
Fir dësen Test ze illustréieren, wäerte mir eis virstellen datt mir amgaang sinn eng Komponent ze kontrolléieren ListComponent déi eng Lëscht vun engem Service gelueden weist ItemsService : e Léierbuch Fall ...
De richtege Wee fir et ze maachen géif esou ausgesinn:

import { ComponentFixture, TestBed } from "@angular/core/testing";
import { anyString, instance, mock, when } from "ts-mockito";
import { ListComponent } from "./list.component";
import { ItemsService } from "./item/items.service";
import { Item } from "./item/item.model";
import { of } from "rxjs/observable/of";
describe("ListComponent", () => {
  let component: ListComponent;
  let fixture: ComponentFixture<ListComponent>;
  let mockedItemsService: ItemsService;
  beforeEach(() => {
    mockedItemsService = mock(ItemsService);
  });
  async function configureTestingModule() {
    await TestBed.configureTestingModule({
      providers: [
        { provide: ItemsService, useValue: instance(mockedItemsService) }
      ],
      declarations: [ListComponent]
    }).compileComponents();
    fixture = TestBed.createComponent(ListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }
  it("should display an empty list", async () => {
    // given
    when(mockedItemsService.loadItems()).thenReturn(of([new Item()]));
    // when
    await configureTestingModule();
    // then
    const nbEl = fixture.debugElement.queryAll(By.css("li")).length;
    expect(nbEl).toEqual(0);
  });
});

Déi éischt Saach, déi Iech schockéiert hätt, war d'Funktioun configureTestingModule ausserhalb vun beforeEach. Tatsächlech wäert et néideg sinn et an eisen Eenheetstester ze nennen an net virdrun fir d'Uruffuerdnung vun de Mockito Funktiounen ze halen, déi an der Preambel ernimmt ginn:

mock -> wann -> Instanz

mock wäert an der genannt ginn beforeEach dann when am Ufank vun der Eenheet Test an endlech instance duerch den Opruff vun configureTestingModule an der Mëtt vun Eenheet Testen.
Wa mir konzentréieren op configureTestingModule, mir gesinn datt et asynchron ass well mir opruffen compileComponents déi e Verspriechen zréckginn. Dëst async "kontaminéiert" eisen Eenheetstest well mir et musse markéieren als async zevill.
Mir wäerten och feststellen datt mir de Provider iwwerlaascht ItemsService fir et d'Instanz vun eisem ts-mockito Spott ze ginn:

{ provide: ItemsService, useValue: instance(mockedItemsService) }

Dëst wäert suergen, datt de TestBed injects dëser simulacrum vun ItemsService op d'Komponente wann et erstallt gëtt.
Endlech simuléiere mir de Retour vun der Method loadItems vum Service sou datt et zréck a Observable&lt;Item[]&gt; eidel a mir kontrolléieren ob d'html Lëscht vun Elementer wierklech eidel ass.

Fall n°4: Testen vun engem High-Level Komponent

Wat ech "Héichniveau" Komponenten nennen, sinn d'Komponenten déi héich an der Komponenthierarchie vun eiser Applikatioun sinn. Komponente Säiten oder mat vill Kanner entspriechen perfekt dëser Bezeechnung. den AppComponent wäert d'Beispill benotzt ginn.
Déi lästeg Saach fir d'Behuele vun dëser Zort Komponente ze kontrolléieren: si hänken per Definitioun vu ville Ënnerkomponenten an Ënnermoduler of an et wäert néideg sinn ALLEGUER fügen se an der TestBed. Fir den AppComponent et ass wéi wann hien bal déi ganz App an Ofhängegkeet gëtt ...
Wann ech Iech ausserdeem gesot hunn, datt déi TestBed.configureTestingModule war lues am Fall 1, et gëtt méi lues wéi d'Zuel vun Ofhängegkeeten eropgeet.
Fir dëse Problem ze iwwerwannen, gëtt et de Parameter schemas zur Verfügung gestallt ginn TestBed.configureTestingModule mam Wäert NO_ERRORS_SCHEMA :

import { TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";
import { NO_ERRORS_SCHEMA } from "@angular/core";
describe("AppComponent", () => {
  let component: ListComponent;
  let fixture: ComponentFixture<ListComponent>;
  beforeEach(async () => {
    TestBed.configureTestingModule({
      providers: [],
      imports: [],
      declarations: [AppComponent],
      schemas: [NO_ERRORS_SCHEMA]
    }).compileComponents();
    fixture = TestBed.createComponent(ListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
  it("should render title in a h1 tag", async () => {
    // given
    const selectorText = compiled.querySelector("h1").textContent;
    // then
    expect(selectorText).toBe("My awesome app");
  });
});

Dëst magescher Parameter, konfiguréiert der TestBed sou datt et kee Feeler geheien wann et de Provider vun enger Komponent oder engem Modul net an der Schabloun (e) vun der getester Komponent (en) benotzt gëtt. Dës Method gëtt genannt shallow testing. Mir testen nëmmen de Komponent AppComponent an net seng subcomponents interpretéieren.
Le NO_ERRORS_SCHEMA soll mat Vorsicht benotzt ginn, well iwwerall applizéiert gëtt, mécht et Ofhängegkeetssystemkontrollen inert a kéint verursaachen datt Dir vergiess hutt e Komponent ze lueden wann et an Ärem Testfall néideg ass.
[bctt tweet = "Am Angular, 'NO_ERRORS_SCHEMA' sollt mat Vorsicht benotzt ginn an nëmme fir Komponenten héich an der Hierarchie ze testen."]

Fall n°5: Päifen, Klassen, de Rescht

D'Päifen, wéi de Rescht vun den Elementer, déi Är Uwendung kéinte komponéieren, gi getest wéi Servicer ouni Angular Ofhängegkeet, nämlech Basis TypeScript Klassen.
Loosst eis e Päif virstellen, deem säin Zil ass a Form vun engem Charakterstring all d'Schlësselen vun engem Objet ze verbannen, deem säi Wäert net null ass. Mir géifen et nennen AliciaKeys... (jo, mir ware ganz houfreg op den Numm)

import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
  name: "aliciaKeys"
})
export class AliciaKeys implements PipeTransform {
  transform(obj: any, arg: string[]): string {
    return obj
      ? Object.entries(obj)
          .filter(([key, value]) => !!value)
          .map(([key]) => key)
          .join(", ")
      : "";
  }
}

Déi befestegt Eenheetstestdatei ass trivial well et eng einfach Instantiatioun vun der Klass ass AliciaKeys mat zwee Appellen aus transform an zwee verschiddene Fäll:

import { AliciaKeys } from "./aliciakeys.pipe";
describe("AliciaKeys", () => {
  it("should return valued keys", () => {
    const result = new AliciaKeys().transform({
      validKey: {},
      inValidKey: null,
      anotherValidKey: 1
    });
    expect(result).toEqual("validKey, anotherValidKey");
  });
  it("should return blank when input is falsy", () => {
    const result = new AliciaKeys().transform(null;
    expect(result).toEqual("");
  });
});

Conclusioun & weider goen

Mir hoffen datt all Fallbeschreiwung Iech och hëlleft besser ze verstoen wéi Dir Är Angular Uwendungen richteg Eenheet testen.
Déi wichteg Punkte fir ze erënneren sinn datt d'Klassen an Angular klassesch TypeScript Klassen bleiwen an datt se einfach getest kënne ginn. Den zweete Punkt ass, datt de TestBed ass net systematesch, datt et nëmme soll benotzt ginn wann Dir keng Wiel hutt. Schlussendlech musse Spott an Angular mat enger Spottbibliothéik geréiert ginn: An eisem Fall ts-mockito.
Dir sollt wëssen datt mir an eisem Projet ongeféier 700 Eenheetstester haten déi méi wéi 30 Sekonnen gedauert hunn. D'Tester streamlinéieren an se beschleunegen gouf entscheedend fir eis.
Mir hunn och dës Sich no Beschleunegung weidergefouert andeems Dir dës lescht passéiert et ass. Dëst ass ganz méiglech andeems Dir d'Instruktiounen am Repo befollegt jest-preset-angular an et spuert vill Zäit!
Mir kommen zréck bei Iech mat méi Detailer iwwer dës Themen wärend eisem "JS-Talks" Feedback (Technologie Iwwerwaachungsdag dee mir all Mount maachen). E puer lescht Fotoen fir ze kucken wéi et ausgesäit:

Geschriwwen Matthew Breton CTO bei JS-Republic