9elements

Testing Angular Apps

Mathias Schäfer, 9elements

2018-10-22
aktualisiert 2019-08-12

Online-Buch: Testing Angular

Der Inhalt dieser Präsentation ist in ein kostenloses Online-Buch eingeflossen:

Testing Angular – A Guide to Robust Angular Applications

9elements

  • Agentur für Software & Design
  • Webseiten und mobile Apps
  • Kundenarbeiten & eigene Produkte
  • Sitz im Bochumer Bermudadreieck

Mathias Schäfer

Arbeitsgebiete

  • JavaScript-Webanwendungen
  • Architektur, Setup & Implementierung
  • Optimierung & Performance
  • Interne und externe Schulungen

Vorstellung und aktueller Stand

  • Eigene Testing-Praxis
  • Einstellung zu Testing
  • Wissensstand
  • Woran hapert es?
  • Ziele

Voraussetzungen

  • Grundwissen in Angular
  • Komponenten, Services
  • Dependency Injection
  • NgRx: Actions, Reducer, Effects

Beispielanwendungen mit Tests

Software-Tests

  • Verifizierung der Funktionalität gemäß Spezifikation
  • Die Software ermöglicht dem User, gewisse Aufgaben zu erfüllen

Software-Tests

  • Agile Entwicklung ermöglichen
  • Move fast, fix things
  • Refactoring ermöglichen, Regressions vermeiden
  • Bessere Architektur
  • Einfacher, lesbarer, wartbarer Code

Arten von Software-Tests

  1. Manuelle Tests
  2. Automatisierte Tests

Methodik

  1. Test-Driven Development (TDD) – Tests First
  2. Testing After the Fact

Ebenen

  1. Unit Tests
  2. Integration Tests
  3. Functional Tests (End-to-End Tests)

Testing in Angular (1)

  • Testbarkeit ist eines der Grundprinzipien
  • Alle Teile sind relativ gut testbar
  • Angular bringt Werkzeuge zum Testen mit

Testing in Angular (2)

  • Angulars Komplexität ist auf die Testbarkeit zurückzuführen
  • Angulars Architektur erschließt sich nicht ohne Testing

Testing in Angular: Praxistipps

  • Wie man Angulars Werkzeuge verwendet, ist einem überlassen
  • Projektweite Konventionen nötig
  • Eigene Helferlein nötig
  • Prägnante, lesbare und verständliche Tests

Testing in Angular: Ziele

  • Testing sollte selbstverständlich sein
  • Testing sollte einfach sein und Spaß machen
  • Übertragbare Beispieltests
  • Copy’n’Paste ist unser Freund

Unit Test in Angular (1)

  • Testet die kleinste mögliche Code-Einheit
  • Testet eine Funktion oder Klasse
  • Testet einen Teil der App:
    Modul, Komponente, Service, Direktive…

Unit Test in Angular (2)

  • Black-Box-Testing
  • Testet das öffentliche Interface oder die Ausgabe
  • Interna werden nicht angefasst, selbst wenn sie technisch erreichbar sind

Unit Test in Angular (3)

  • Testet die Einheit möglichst isoliert
  • Abhängigkeiten werden durch Mock-Objekte ersetzt
  • Verifizieren des Aufrufs der Abhängigkeiten
  • Keine Nebenwirkungen (Side Effects, z.B. HTTP-Requests)

Terminologie

Stubs
Ersatz für eine Abhängigkeit
Vordefinierte Eigenschaften & Rückgabewerte
Zustand verifizieren
Mocks
Wie Stubs
Erwartungen gegen den Mock ausführen
Zustand & Verhalten verifizieren

Quelle: Angular-Buch, dpunkt.verlag, 1. Auflage 2017, S. 383

Unit Test in Angular (3)

  • Vorteil: Effektiv und gezielt
  • Vorteil: Einfach, nah am Code
  • Nachteil: Sehr technisch, wenig aus User-Sicht
  • Nachteil: Herauslösen aus dem Kontext
  • Nachteil: Mocking-Aufwand

Unit Test in Angular (4)

  • Testet einen Teil der App
    (Modul, Komponente, Service, Direktive…)
  • *.spec.ts
  • Karma Test Runner
  • Jasmine für Test Suites und Assertions
  • TestBed

Integration Tests

  • Testet mehrere zusammenhängende Einheiten
  • Testet eine Einheit mitsamt Abhängigkeiten / Kindern
  • Meist eine komplexe Einheit auf hoher Ebene

Integration Test in Angular

  • Testet einen Teil der App auf hoher Ebene
  • Modul
  • Komponente mit Kindern
  • Service mit anderen Services

Integration Test in Angular

  • Zusammenhängendes wird nicht künstlich getrennt
  • Komplexer, immer noch nah am Code
  • Mehr aus User-Sicht
  • Nebenwirkungen schwer zu vermeiden

End-to-End Tests

  • Testet die gesamte App aus User-Sicht
  • User ↔ Frontend ↔ Backend ↔ DB
  • Implementierungsdetails sind irrelevant

End-to-End Test in Angular

  • Startet einen Browser, navigiert zur App
  • Simuliert Eingaben, z.B. Klicks auf Links
  • Operiert nicht auf Code-Ebene
  • Schaut nur die HTML-Ausgabe an

Unit Test ausführlich

  • Wir erstellen einen Counter
  • npm install -g @angular/cli
  • ng new counter
  • "strict": true in tsconfig.json aktivieren
  • ng generate component counter

Counter-Funktionalität

  • Komponente hält aktuellen Count-Wert
  • Anzeige des Count-Werts
  • Button zum Erhöhen des Count-Wertes

Aufbau eines Unit Test

  • Jasmine describe(), beforeEach(), it()
  • TestBed konfigurieren
  • configureTestingModule, createComponent
  • fixture.detectChanges()

Funktionalität einer Komponente testen

  • Wie testen wir die Funktionalität?
  • HTML-DOM ansehen!
  • Komponente rendern
  • Elemente heraussuchen
  • Inhalte prüfen

Komponente in einem Unit Test

  • ComponentFixture, Komponenteninstanz, DebugElement
  • fixture
  • fixture.componentInstance
  • fixture.nativeElement

Elemente heraussuchen

  • fixture.debugElement.query(By.css('…'))
  • fixture.debugElement.queryAll(By.css('…'))

Wie Elemente markieren und finden?

  • IDs
  • Klassen: class="qa-count"
  • data-Attribute: data-testid="count"
  • Empfehlung: data-testid
  • query(By.css('[data-testid="count"]'))

Textinhalte überprüfen (1)

  • query() liefert ein DebugElement
  • Wrapper um das echte DOM-Element
  • debugElement.nativeElement liefert den DOM-Knoten

Textinhalte überprüfen (2)

  • const text =
    debugElement.nativeElement.textContent;
  • expect(text).toBe('…');
  • expect(text).toContain('…');

Counter-Test

  • ✅ it('renders the initial count', …)
  • ❌ it('increments', …)

Ereignisse simulieren

  • Element heraussuchen
  • triggerEventHandler aufrufen

debugElement.triggerEventHandler(
  type: string,
  event: Event
)

Synthetisches Event-Objekt


debugElement.triggerEventHandler('click', null)
debugElement.triggerEventHandler('click', {
  preventDefault() {},
  stopPropagation() {},
  target: debugElement.nativeElement,
  currentTarget: debugElement.nativeElement,
  pageX: 100,
  pageY: 200
})

Interaktivität testen

  1. Interaktives Element heraussuchen (Button)
  2. Ereignis simulieren
  3. Manuell neu rendern: fixture.detectChanges()
  4. Ausgabe prüfen

Counter-Test

  • ✅ it('renders the initial count', …)
  • ✅ it('increments', …)

Counter-Funktionalität erweitern

  • Decrement
  • Reset: Eingabefeld und Reset-Button

Counter-Reset testen

  1. Eingabefeld heraussuchen und value setzen
  2. Klick auf Reset-Button simulieren
  3. detectChanges()
  4. Ausgabe prüfen

Helferlein zum Testen von Komponenten

  • findEl(fixture, testId): DebugElement
  • findEls(fixture, testId): DebugElement[]
  • getText(fixture, testId): string
  • expectText(fixture, testId, text)
  • setFieldValue(fixture, testId, text)
  • click(fixture, testId)

Input und Outputs

  • Counter bekommt einen Input: startCount
  • Counter bekommt einen Output: countChange

<app-counter
  [startCount]="5"
  (countChange)="logCount($event)"
></app-counter>

Inputs im Test setzen

Inputs sind Eigenschaften der Komponenteninstanz


component.startCount = startCount;
component.ngOnChanges();
fixture.detectChanges();

Output testen (1)

  • Outputs sind EventEmitter
  • EventEmitter sind Observables
  • component.countChange.subscribe()

Output testen (2)


let actualValue: number | undefined;
component.countChange.subscribe((value: number) => {
  actualValue = value;
});

click(fixture, 'increment-button');
expect(actualValue).toBe(1);

Einfache Komponenten: ✅ getestet

  • Ausgabe testen
  • User-Eingaben simulieren
  • Inputs & Outputs
  • Verwendet Helferlein!

Komponente als Black Box testen

  • Nur ins DOM schauen und Ereignisse auslösen
  • Nur über Inputs und Outputs mit der Komponente sprechen
  • Keine Methoden aufrufen
  • Nicht auf Eigenschaften zugreifen
    (auch nicht wenn sie öffentlich sind)

Tests debuggen

  • Chrome benutzen, Developer Tools öffnen
  • Test-Focus setzen mit fdescribe() und fit()
  • console.log(…) ist Gold wert
  • Debug-Ausgaben in Lifecycle-Methoden, Handlern und im Template

Komplexe Komponenten

  • Eigenständig vs. verbunden
  • »Smart« vs. »dumb«
  • Abhängigkeiten:
    • Verschachtelte Komponenten
    • Services
    • NgRx Store

Verschachtelte Komponenten testen

  • Unit Test – Shallow Rendering –
    Kinder nicht rendern
  • Integration Test – Deep Rendering –
    Kinder mitrendern

Deep Rendering

  • Beispiel: app.component.html referenziert <app-counter>
  • Standardmäßig werden die Kinder mitgerendert
  • Alle Komponenten müssen im Testmodul deklariert werden

Shallow Rendering (1)

  • Beispiel AppComponent
  • Kinder nicht mitrendern:
    schemas: [ NO_ERRORS_SCHEMA ]
  • Wrapper-Elemente bleiben leer, Kindkomponenten werden nicht instantiiert
  • <app-counter …></app-counter>

Shallow Rendering (2):
Was testen?

  1. Kindkomponente wird gerendert
    (Wrapper app-counter ist vorhanden)
  2. Input-Daten werden korrekt übergeben
  3. Auf Events (Outputs) wird korrekt reagiert

Shallow Rendering (3):
Kindkomponente vorhanden?


const el =
  fixture.debugElement.query(By.css('app-counter'));
expect(el).toBeTruthy();

Shallow Rendering (4):
Kindkomponente vorhanden?


// Helferlein: Wirft einen Fehler, wenn nichts gefunden
const el = findComponent(fixture, 'app-counter');
expect(el).toBeTruthy();
// Oder
expect().nothing();

Shallow Rendering (5):
Input testen

DebugElement hat eine Eigenschaft properties


<app-counter [startCount]="5"></app-counter>

const el = findComponent(fixture, 'app-counter');
expect(el.properties.startCount).toBe(5);

Shallow Rendering (6):
Output testen

  • Outputs sind aus Sicht der Elternkomponente Ereignisse
  • Simulieren mit dem bekannten triggerEventHandler

Shallow Rendering (7):
Output testen

  • (countChange)="handleCountChange($event)"
  • triggerEventHandler('countChange', 5)
  • Auswirkung prüfen (z.B. mit Jasmine Spies)

Komponente mit Service-Abhängigkeit (1)

Beispiel ServiceCounterComponent


class ServiceCounterComponent {
  constructor(private counterService: CounterService) {
    this.count$ = this.counterService.getCount();
  }
}

Komponente mit Service-Abhängigkeit (2)

  • Unit Test – Service wird gemockt
  • Integration Test – Service wird mitgetestet

Service-Abhängigkeit mocken

  • Verschiedene Mocking-Strategien 🤷‍♀️
  • Testing with the real service
  • Mocking with fake classes
  • Mocking by overriding functions
  • Mock by using a real instance with Spy

Anforderungen an Mocks

  • Original darf nie aufgerufen werden (Nebenwirkungen!)
  • ⇒ Es darf nicht möglich sein, das Überschreiben einer Methode zu vergessen
  • Mock und Original müssen auf dem gleichen Stand sein
  • ⇒ Mock muss eine Typableitung des Originals sein

Anforderungen erfüllt?

  • ⛈ Testing with the real service
  • ⛅️ Mocking with fake classes
  • 🌧 Mocking by overriding functions
  • 🌧 Mock by using a real instance with Spy

🙍‍♀️🤦‍♀️

Service-Abhängigkeit mocken

  • 👩‍💻 Basis: Mocking with fake classes
  • 👩‍🔬 Entweder eine Klasse oder Instanz
  • 👩‍🔧 Typableitung hinzufügen
  • 💆‍♀️ 🌈 ☀️

Typ vorbereiten

Einzelne Methoden


type PartialCounterService = Pick<
  CounterService,
  'getCount' | 'increment' | 'decrement' | 'reset'
>;

Typ vorbereiten (Alternative)

Alle öffentlichen Methoden


type PartialCounterService = Pick<
  CounterService,
  keyof CounterService
>;

☀️ Mock-Service als Klasse


class MockCounterService implements PartialCounterService {
  getCount() {
    return of(count);
  }
  increment() {}
  decrement() {}
  reset() {}
}

☀️ Mock-Service als Objekt


const mockCounterService: PartialCounterService = {
  getCount() {
    return of(count);
  },
  increment() {},
  decrement() {},
  reset() {}
};

Mock anstelle des Originals verwenden

Im Testing Module:


providers: [
  { provide: CounterService, useClass: MockCounterService }
]

providers: [
  { provide: CounterService, useValue: mockCounterService }
]

Interaktion mit dem Mock testen

  • Mock liefert feste Rückgabewerte
  • Mock erwartet gewisse Parameter
  • Parameter-Übergabe testen mit Jasmine Spies

Jasmine Spy

  • Funktion, die alle Aufrufe aufzeichnet
  • Später ist Prüfung möglich
  • Wurde der Spy aufgerufen? Wie oft?
  • Wurde der Spy mit gewissen Parametern aufgerufen?

Unabhängigen Spy erzeugen


const spy = jasmine.createSpy('name');
const spy = jasmine.createSpy('name').and.returnValue(…);
const spy = jasmine.createSpy('name').and.callFake((…) => {…});

Spy wrappt eine vorhandene Methode


spyOn(object, 'method');
spyOn(object, 'method').and.callThrough();
spyOn(object, 'method').and.returnValue(value);

Spies verifizieren


expect(spy).toHaveBeenCalled();
expect(spy).not.toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(5);
expect(spy).toHaveBeenCalledWith(param1, …);
expect(object.method).toHaveBeenCalled();
expect(object.method).toHaveBeenCalledWith(param1, …);

Spies am Mock-Service installieren


spyOn(mockCounterService, 'getCount').and.callThrough();
spyOn(mockCounterService, 'increment');
spyOn(mockCounterService, 'decrement');
spyOn(mockCounterService, 'reset');

Mock-Service als Spy-Objekt

jasmine.createSpyObj()


const mockCounterService = jasmine.createSpyObj<PartialCounterService>(
  'CounterService', [
  'getCount', 'increment', 'decrement', 'reset',
]);
mockCounterService.getCount.and.returnValue(of(count));

Spies verifizieren


expect(mockCounterService.getCount).toHaveBeenCalled();
expect(mockCounterService.increment).toHaveBeenCalled();
expect(mockCounterService.decrement).toHaveBeenCalled();
expect(mockCounterService.reset).toHaveBeenCalledWith(newCount);

Service-Mocking: Fazit

  • Mocking ist aufwändig und erfordert Übung
  • Services lassen sich gut mocken …
  • … wenn das Interface übersichtlich und die Funktionalität klar sind
  • Sinnvolle Testdaten (Stubs) sind nötig

Komponente mit NgRx-Store-Abhängigkeit

NgRxCounterComponent


class NgRxCounterComponent {
  constructor(private store: Store<AppState>) {
    this.count$ = store.pipe(select('counter'));
  }
}

Mock-Store bereitstellen (1)

Offizielle Methode


provideMockStore({ initialState: {…}, selectors: {…} })

Mock-Store bereitstellen (2)


TestBed.configureTestingModule({
  declarations: [ NgRxCounterComponent ],
  providers: [
    provideMockStore({ initialState: mockState })
  ]
}).compileComponents();

Mock-State erzeugen


const mockState: Partial<AppState> = {
  counter: 1
};

Store-Abhängigkeit: Was testen?

  • Komponente zieht sich Daten aus dem Store
  • Komponente transformiert ggf. diese Daten
  • Komponente rendert diese Daten
  • Komponente dispatcht Actions

Dispatch von Actions testen (1)

In beforeEach einen Spy auf MockStore#dispatch installieren


store = TestBed.get(Store);
spyOn(store, 'dispatch');

Dispatch von Actions testen (2)


it('resets the count', () => {
  const newCount = 15;
  findEl(fixture, 'reset-input').nativeElement.value = newCount;
  click(fixture, 'reset-button');
  expect(store.dispatch).toHaveBeenCalledWith(reset({ count: newCount }));
});

Verschiedene States testen

  • Wenn der State komplex sein kann, müssen alle Fälle getestet werden
  • Pro Spec ein anderer State
  • Flexible setup-Funktion statt fester beforeEach-Logik

Setup-Funktion


function setup(mockState: Partial<AppState>) {
  TestBed.configureTestingModule({
    declarations: [ NgRxCounterComponent ],
    providers: [ provideMockStore({ initialState: mockState }) ]
  }).compileComponents();

  const store: Store<AppState> = TestBed.get(Store);
  spyOn(store, 'dispatch');

  const fixture = TestBed.createComponent(NgRxCounterComponent);
  fixture.detectChanges();

  return { fixture, store };
}

Verschiedene States testen


it('renders the data from the store', () => {
  const mockState: Partial<AppState> = { counter: 1 };
  const { fixture, store } = setup(mockState);
  expectText(fixture, 'count', String(mockState.counter));
})

Baut Helferlein, die komplexen Mock-State generieren

Zusammenfassung Komponenten-Tests

  1. Eigenständige Komponenten:
    Ausgabe, Interaktivität, Inputs, Outputs
  2. Komponenten mit Service-Abhängigkeit:
    DI, Mocking, Spies, Mock-Daten
  3. Komponenten mit Store-Abhängigkeit:
    DI, Mock State + Store, Action-Dispatch

Weitere Teile der Anwendung

  • Services
  • Effects
  • Reducer
  • (Pipes, Directives, Resolver…)

Services – Was testen?

  • Methoden liefern Werte zurück
  • Methodenaufrufe ändern privaten State
    → Indirekt testen
  • Interaktion mit Abhängigkeiten (z.B. HttpClient)

Services testen: CounterService

  • Standard-TestBed
  • Eine Spec für jede öffentliche Methode:
    getCount, increment, decrement, reset
  • Auswirkung testen durch getCount-Aufruf

Services mit Abhängigkeit testen:
CounterApiService


TestBed.configureTestingModule({
  imports: [ HttpClientTestingModule ],
  providers: [ CounterApiService ]
});

HttpClientTestingModule (1)

HTTP-Requests finden


const httpMock: HttpTestingController =
  TestBed.get(HttpTestingController);

const request = httpMock.expectOne({
  method: 'GET', url: expectedURL
});

const predicate = (candidateRequest) =>
  candidateRequest.method === 'GET';
const request = httpMock.expectOne(predicate);
const requests = httpMock.match(predicate);

HttpClientTestingModule (2)

Gefundene Requests beantworten


request.flush(serverResponse);

Fehler simulieren


request.error(
  new ErrorEvent('API error'),
  { status: 404, statusText: 'Not Found' }
);

HttpClientTestingModule (3)

Verfizieren, dass alle Requests gefunden und beantwortet wurden


httpMock.verify();

Fazit: Services testen

  • Relativ einfach
  • Nichts fundamental Neues
  • Gleicher Aufwand wie beim Service-Mocking für Komponenten-Tests

NgRx Effects

  • Die Redux-Architektur lässt es offen, wie Nebenwirkungen (Side Effects) umgesetzt werden
  • Effects sind eine hervorragende Lösung
  • Alle anderen Lösungen sind m.E. komplizierter oder schwerer zu testen

Grundschema eines Effects

  • WENN eine gewisse Action eintritt
  • DANN Nebenwirkung ausführen
  • DANN Erfolgs-Action ausgeben
  • ODER Fehler-Action ausgeben

CounterEffects saveOnChange$

  • WENN Action increment eintritt
  • DANN aktuellen Count aus dem Store auslesen
  • DANN den Count an den Server senden
  • DANN saveSuccess ausgeben
  • ODER saveError ausgeben

Effects: Umsetzung

  • Ein Effect ist ein Observable
  • Bildet Actions auf Actions ab
  • Input:
    • Actions: Observable<Action>
    • ggf. Store: Observable<AppState>
  • Output: Observable<Action>

Effect schematisch

  1. Observable<Action>
  2. Filter mit ofType()
  3. State holen mit withLatestFrom(store)
  4. Nebenwirkung (Service-Call)
  5. Map auf Success-Action
  6. catchError mit Error-Action

Effects aus Sicht von Angular

  • CounterEffects ist eine Klasse mit Eigenschaften vom Typ Observable<Action>
  • Die Klasse nimmt an der Dependency Injection Teil (@Injectable)
  • Abhängigkeiten sind deklariert

Effects: Abhängigkeiten


public constructor(
  private actions$: Actions,
  private store: Store<AppState>,
  private counterApiService: CounterApiService
) {}

Effects: Abhängigkeiten mocken


TestBed.configureTestingModule({
  providers: [
    { provide: Actions, useValue: ??? },
    { provide: Store, useValue: ??? },
    { provide: CounterApiService, useValue: ??? },
    CounterEffects
  ]
});

Effects testen

  1. Input-Observable mit Actions bereitstellen (actions$)
  2. ggf. Mock-Store bereitstellen (store)
  3. Service-Mock bereitstellen (counterApiService)
  4. Output-Observable abonnieren, Werte prüfen
  5. Service-Mock verifizieren

Input-Observable mit Actions bereitstellen


import { from, of } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing';

const action = reset({ count: 123 });
provideMockActions(of(action))

const actions = [ reset({ count: 123 }) ];
provideMockActions(from(actions))

Mock-State erzeugen


const mockState: Partial<AppState> = {
  counter: 1
};

Mock-Store bereitstellen

  • provideMockStore()
  • Es geht noch einfacher, wenn dispatch und select nicht aufgerufen werden
  • Der Store ist ein Observable
  • { provide: Store, useValue: of(mockState) }

Service-Mock bereitstellen

Mock für CounterApiService


type PartialCounterApiService = Pick<CounterApiService, 'saveCounter'>;

const mockCounterApi: PartialCounterApiService = {
  saveCounter() {
    return of({});
  }
};

spyOn(mockCounterApi, 'saveCounter').and.callThrough();

Mock für CounterApiService (Alternative)


const mockCounterApi = jasmine.createSpyObj<CounterApiService>(
  'CounterApiService',
  ['saveCounter']
);
mockCounterApi.saveCounter.and.returnValue(of({}));

Output-Observable prüfen


counterEffects.saveOnChange$.subscribe((outputAction) => {
  expect(outputAction).toEqual(saveSuccess());
});

counterEffects.saveOnChange$
  .pipe(toArray())
  .subscribe((outputActions) => {
    expect(outputActions).toEqual([ saveSuccess() ]);
  });

Output-Observable prüfen: Helferlein


function expectActions(
  effect: Observable<Action>, actions: Action[]
) {
  effect.pipe(toArray()).subscribe(
    (actualActions) => {
      expect(actualActions).toEqual(actions);
    },
    fail
  );
}

expectActions(counterEffects.saveOnChange$, [
  saveSuccess()
]);

Service-Mock verifizieren


expect(mockCounterApi.saveCounter)
  .toHaveBeenCalledWith(mockState.counter);

Fehlerfall testen

Zweiter Service-Mock, der einen Fehler wirft


const mockCounterApiError: PartialCounterApiService = {
  saveCounter() {
    return throwError(apiError);
  }
};

Eine Error-Action erwarten


expectActions(counterEffects.saveOnChange$, [
  saveError({ error: apiError })
]);

Komplexe Effects testen

  • Die meisten Effects haben eine einfache RxJS-Logik
  • Input Actions + Store + Nebenwirkung
    → Output Action(s)
  • Komplexen Effect in mehrere einfache zerlegen
  • Marble Testing

Effects testen: Zusammenfassung

  • Setup erfordert tieferes Verständnis von RxJS und NgRx
  • Mocking: Input-Actions, Store, Service(s)
  • Helferlein sinnvoll
  • Dann relativ wenig Arbeit

Reducer testen (1)

  • Reducer sind Pure Functions
  • Daher einfach zu testen
  • Werte rein, Werte raus
  • Nur Stubs, keine Mocks

Reducer testen (2)


function partReducer(state: StatePart, action: Action): StatePart {}

const state: StatePart = { … };
const action = someAction();
const newState: StatePart = { … };
expect(partReducer(state, action)).toEqual(newState);

Reducer testen (3)

  • Beispiel counterReducer
  • Initialisierung: state = initialState
  • Default-Fall (return state)
  • State-Änderung bei den relevanten Actions

Immutability in Reducern

  • Reducer dürfen den State nicht ändern
  • Müssen einen neues Objekt (Kopie) erzeugen
  • { ...state, property: newValue }

Immutability-Helferlein

Code Coverage

100% Code Coverage

  • 100% Coverage ist möglich und sinnvoll
  • Bei den letzten Prozent wird es erst interessant
  • Edge Cases und schwer zu testbare Fälle

Wert der Code Coverage

  • Jede Zeile wurde mindestens einmal ausgeführt
  • Bedeutet nicht, dass alle Fälle sinnvoll getestet wurden

End-to-End Tests in Angular

End-to-End Test

  • Navigiert zu einer URL
  • Sucht Elemente heraus
  • Simuliert Maus- und Tastatur-Eingaben
  • Testet Elementinhalte

End-to-End Tests starten

  • ng e2e
  • e2e/app.e2e-spec.ts

Protractor: Browser steuern

Protractor: Einzelne Elemente finden


element(by.id('…'))
element(by.name('…'))
element(by.className('…'))
element(by.css('…'))
$('…')
element(by.css('[data-testid="count"]'))
findEl('count')

Helferlein: findEl

Protractor: Viele Elemente finden


element.all(by.id('…'))
element.all(by.name('…'))
element.all(by.className('…'))
element.all(by.css('…'))
$$('…')
element.all(by.css('[data-testid="count"]'))
findEls('count')

Helferlein: findEls

Protractor: Textinhalt lesen


// <h1 data-testid="count">Hello</h1>
const el = findEl('heading');
expect(heading.getText()).toBe('Hello');

Protractor: Klicks


// <button data-testid="increment-button">+</button>
findEl('increment-button').click();

Protractor: Tastatureingaben


// <input data-testid="reset-input">
findEl('reset-input').sendKeys('123');

Protractor: Page Objects

  • Page Object ist eine einfache Klasse, die eine Seite repräsentiert
  • Page Object: Low-level, Test: High-Level
  • Ziel: Prägnanz und Lesbarkeit des Tests erhöhen
  • Wenn sich das Markup ändert:
    Page Object ändern, Test nicht

Protractor: Page Objects

  • *.po.ts
  • Einfache Klasse mit Methoden, meist Element-Getter
  • Selektoren (data-testid-Namen)
  • findEl- und findEls-Aufrufe
  • Komplexere Eingabesequenzen

Counter-App

End-to-End Tests: Fallstricke

  • Alle WebDriver-Aktionen sind asynchron und geben Promises zurück
  • jasminewd ermöglicht es Tests zu schreiben, als wären die Aktionen synchron
  • E2E-Tests sehen Unit-Tests ähnlich, laufen aber fundamental anders

End-to-End Tests: Asynchronität


const el = findEl(…);
el.click();
expect(el.getText()).toBe('Hello');

Intern:


findEl(…)
  .then((el) => el.click())
  .then((el) => el.getText())
  .then((text) => expect(text).toBe('Hello');

End-to-End Tests: Fazit

  • Äußerst effektiv, um ein Feature unter realen Bedingungen zu testen
  • Hochkomplex, daher unzuverlässig und fehleranfällig
  • Konventionen nötig
  • Protractor stammt aus Angular-1-Zeiten
  • Simulierte Synchronität ist schwarze Magie

Testen und Testbarkeit

  • Testing lehrt testbaren Code zu schreiben
  • Testbarer Code ist besserer Code
  • Do one thing and do it well
  • Logik in kleine, wohldefinierte Einheiten aufbrechen
  • Einheiten einzeln und im Verbund testen

9elements