9elements
Testing Angular Apps
Mathias Schäfer, 9elements
2018-10-22 aktualisiert 2019-08-12
Agentur für Software & Design
Webseiten und mobile Apps
Kundenarbeiten & eigene Produkte
Sitz im Bochumer Bermudadreieck
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
Manuelle Tests
Automatisierte Tests
Methodik
Test-Driven Development (TDD) – Tests First
Testing After the Fact
Ebenen
Unit Tests
Integration Tests
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)
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('…');
✅ 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
Interaktives Element heraussuchen (Button)
Ereignis simulieren
Manuell neu rendern: fixture.detectChanges()
Ausgabe prüfen
✅ it('renders the initial count', …)
✅ it('increments', …)
Counter-Funktionalität erweitern
Decrement
Reset: Eingabefeld und Reset-Button
Counter-Reset testen
Eingabefeld heraussuchen und value
setzen
Klick auf Reset-Button simulieren
detectChanges()
Ausgabe prüfen
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?
Kindkomponente wird gerendert (Wrapper app-counter
ist vorhanden)
Input-Daten werden korrekt übergeben
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
Eigenständige Komponenten: Ausgabe, Interaktivität, Inputs, Outputs
Komponenten mit Service-Abhängigkeit: DI, Mocking, Spies, Mock-Daten
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
)
Standard-TestBed
Eine Spec für jede öffentliche Methode:
getCount
, increment
, decrement
, reset
Auswirkung testen durch getCount
-Aufruf
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
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
Observable<Action>
Filter mit ofType()
State holen mit withLatestFrom(store)
Nebenwirkung (Service-Call)
Map auf Success-Action
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
Input-Observable mit Actions bereitstellen (actions$
)
ggf. Mock-Store bereitstellen (store
)
Service-Mock bereitstellen (counterApiService
)
Output-Observable abonnieren, Werte prüfen
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 }
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
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