Gerade in der Welt von JavaScript kann testen eine mühsame Aufgabe sein. Asynchronität, Timer und Events führen dazu, dass Tests schnell unübersichtlich werden. Um diese Aufgabe zu erleichtern, gibt es in Angular die sogenannten Component Harness
Die Idee ist simple: anstatt sich mit Events und generischen DOM Methoden herumzuärgern definiert eine Komponente eine eindeutige Schnittstelle über die mit dieser Komponente interagiert werden kann. Ein solcher Component Harness kann entweder von uns selber geschrieben werden (für unsere eigenen Komponenten) oder verwendete Bibliotheken bringe diese zum direkten los testen mit, wie z.B. Angular Materials.
Die Verwendung ist denkbar einfach. Im ersten Schritt benötigen wir in unserem Unit-Test einen sogenannten RootLoader der später die einzelnen UI Komponenten für uns lädt. Dieser RootLoader wird auf Basis der aktuell zu testenden Komponente initialisiert:
fixture = TestBed.createComponent(AppComponent);
rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);
In unseren einzelnen Tests besteht nun die Möglichkeit, auf Basis der Harness Klassen die entsprechenden Elemente innerhalb unserer Komponente zugreifbar zu machen. Dies geschieht über eine entsprechend typisiert Methode, die zusätzlich auch CSS Selektoren entgegennimmt:
it('...', async() => {
const input = await rootLoader.getHarness(MatInputHarness.with({selector: '#form1 input'}))
const button = await rootLoader.getHarness(MatButtonHarness);
Zwei wichtige Punkte: 1) unsere Testmethode wird als asynchrone Methode deklariert (async), um bei der Interaktion mit den Elementen 2) einfach per „await“ auf die asynchrone Abarbeitung warten zu können. Die meisten Methoden in Bezug auf den Test-Harness sind asynchron (Rückgabetyp ist in aller Regel ein „Promise„) und werden mit einem „await“ blockiert. Technisch könnte man hier auch eine altmodische Callbackmethode deklarieren: rootLoader.getHarness(…).then( component => {…}) , was aber bekanntermaßen zu vielen verschachtelten Callbackbackmethoden führen kann.
Die typisierten Component Harness bieten nun spezifische Methoden, um mit der zugrundeliegenden Komponente zu interagieren
await input.setValue('Tim');
expect(await checkbox.isDisabled()).toBeFalse();
const logicSpy = spyOn(app, 'someLogic').and.callThrough();
await checkbox.check();
expect(logicSpy).toHaveBeenCalledTimes(1);
Hier noch mal alles, wie immer, live und in Farbe: